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 7a8c421..05477cd 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 @@ -1,5 +1,6 @@ package com.alibaba.testable.agent.handler; +import com.alibaba.testable.agent.util.ClassUtil; import com.alibaba.testable.core.util.LogUtil; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassWriter; @@ -15,30 +16,33 @@ import java.util.Iterator; */ abstract public class BaseClassHandler implements Opcodes { - protected static final String REF_GET_INSTANCE = "getInstance"; + protected static final String TESTABLE_REF = "__testable"; + protected static final String GET_TESTABLE_REF = "testableIns"; protected static final String VOID_ARGS = "()"; protected static final String VOID_RES = "V"; protected String mockClassName; - protected boolean wasTransformed(ClassNode cn, String refName, String refDescriptor) { + protected boolean markTransformed(ClassNode cn, String refName, String refDescriptor) { Iterator iterator = cn.fields.iterator(); if (iterator.hasNext()) { if (refName.equals(iterator.next().name)) { // avoid duplicate injection LogUtil.verbose("Duplicate injection found, ignore " + cn.name); - return true; + return false; } } cn.fields.add(new FieldNode(ACC_PRIVATE | ACC_STATIC, refName, refDescriptor, null, null)); - return false; + return true; } public byte[] getBytes(byte[] classFileBuffer) throws IOException { ClassReader cr = new ClassReader(classFileBuffer); ClassNode cn = new ClassNode(); cr.accept(cn, 0); - transform(cn); + if (markTransformed(cn, TESTABLE_REF, ClassUtil.toByteCodeClassName(mockClassName))) { + transform(cn); + } ClassWriter cw = new ClassWriter( 0); cn.accept(cw); return cw.toByteArray(); diff --git a/testable-agent/src/main/java/com/alibaba/testable/agent/handler/BaseClassWithContextHandler.java b/testable-agent/src/main/java/com/alibaba/testable/agent/handler/BaseClassWithContextHandler.java new file mode 100644 index 0000000..3a291ec --- /dev/null +++ b/testable-agent/src/main/java/com/alibaba/testable/agent/handler/BaseClassWithContextHandler.java @@ -0,0 +1,76 @@ +package com.alibaba.testable.agent.handler; + +import com.alibaba.testable.agent.util.ClassUtil; +import org.objectweb.asm.tree.*; + +/** + * @author flin + */ +abstract public class BaseClassWithContextHandler extends BaseClassHandler { + + private static final String CLASS_TESTABLE_TOOL = "com/alibaba/testable/core/tool/TestableTool"; + private static final String CLASS_TESTABLE_UTIL = "com/alibaba/testable/core/util/TestableUtil"; + private static final String CLASS_MOCK_CONTEXT = "com/alibaba/testable/agent/model/MockContext"; + private static final String FIELD_TEST_CASE = "TEST_CASE"; + private static final String FIELD_SOURCE_METHOD = "SOURCE_METHOD"; + private static final String FIELD_MOCK_CONTEXT = "MOCK_CONTEXT"; + private static final String FIELD_PARAMETERS = "parameters"; + private static final String METHOD_CURRENT_TEST_CASE_NAME = "currentTestCaseName"; + private static final String METHOD_CURRENT_SOURCE_METHOD_NAME = "currentSourceMethodName"; + private static final String METHOD_GET_TEST_CASE_MARK = "getTestCaseMark"; + private static final String SIGNATURE_CURRENT_TEST_CASE_NAME = "(Ljava/lang/String;)Ljava/lang/String;"; + private static final String SIGNATURE_CURRENT_SOURCE_METHOD_NAME = "()Ljava/lang/String;"; + private static final String SIGNATURE_GET_TEST_CASE_MARK = "()Ljava/lang/String;"; + private static final String SIGNATURE_PARAMETERS = "Ljava/util/Map;"; + private static final String CLASS_MAP = "java/util/Map"; + private static final String METHOD_MAP_GET = "get"; + private static final String SIGNATURE_MAP_GET = "(Ljava/lang/Object;)Ljava/lang/Object;"; + + protected AbstractInsnNode[] handleTestableUtil(ClassNode cn, MethodNode mn, AbstractInsnNode[] instructions, int i) { + if (instructions[i].getOpcode() == GETSTATIC) { + FieldInsnNode fieldInsnNode = (FieldInsnNode)instructions[i]; + if (isTestableUtilField(fieldInsnNode)) { + instructions = replaceTestableUtilField(cn, mn, instructions, fieldInsnNode.name, i); + } + } + return instructions; + } + + private boolean isTestableUtilField(FieldInsnNode fieldInsnNode) { + return fieldInsnNode.owner.equals(CLASS_TESTABLE_TOOL) && + (fieldInsnNode.name.equals(FIELD_TEST_CASE) || fieldInsnNode.name.equals(FIELD_SOURCE_METHOD) || + fieldInsnNode.name.equals(FIELD_MOCK_CONTEXT)); + } + + private AbstractInsnNode[] replaceTestableUtilField(ClassNode cn, MethodNode mn, AbstractInsnNode[] instructions, + String fieldName, int pos) { + InsnList il = new InsnList(); + if (FIELD_TEST_CASE.equals(fieldName)) { + il.add(new LdcInsnNode(ClassUtil.toDotSeparatedName(cn.name))); + il.add(new MethodInsnNode(INVOKESTATIC, CLASS_TESTABLE_UTIL, METHOD_CURRENT_TEST_CASE_NAME, + SIGNATURE_CURRENT_TEST_CASE_NAME, false)); + } else if (FIELD_SOURCE_METHOD.equals(fieldName)) { + il.add(new MethodInsnNode(INVOKESTATIC, CLASS_TESTABLE_UTIL, METHOD_CURRENT_SOURCE_METHOD_NAME, + SIGNATURE_CURRENT_SOURCE_METHOD_NAME, false)); + } else if (FIELD_MOCK_CONTEXT.equals(fieldName)) { + il.add(new FieldInsnNode(GETSTATIC, CLASS_MOCK_CONTEXT, FIELD_PARAMETERS, SIGNATURE_PARAMETERS)); + il.add(new VarInsnNode(ALOAD, 0)); + il.add(new MethodInsnNode(INVOKESPECIAL, cn.name, METHOD_GET_TEST_CASE_MARK, + SIGNATURE_GET_TEST_CASE_MARK, false)); + il.add(new MethodInsnNode(INVOKEINTERFACE, CLASS_MAP, METHOD_MAP_GET, + SIGNATURE_MAP_GET, true)); + } + if (il.size() > 0) { + mn.instructions.insert(instructions[pos], il); + mn.instructions.remove(instructions[pos]); + } + return mn.instructions.toArray(); + } + + /** + * get mark compose by TestClass and TestCase + * @return test case mark + */ + abstract protected String getTestCaseMark(); + +} 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 c406b53..de9b8cd 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 @@ -4,6 +4,8 @@ import com.alibaba.testable.agent.constant.ConstPool; import com.alibaba.testable.agent.tool.ImmutablePair; import com.alibaba.testable.agent.util.AnnotationUtil; import com.alibaba.testable.agent.util.ClassUtil; +import com.alibaba.testable.core.util.InvokeRecordUtil; +import com.alibaba.testable.core.util.TestableUtil; import org.objectweb.asm.Type; import org.objectweb.asm.tree.*; @@ -16,9 +18,8 @@ import static com.alibaba.testable.agent.util.ClassUtil.toDotSeparateFullClassNa /** * @author flin */ -public class MockClassHandler extends BaseClassHandler { +public class MockClassHandler extends BaseClassWithContextHandler { - private static final String REF_INSTANCE = "_instance"; private static final String CLASS_INVOKE_RECORD_UTIL = "com/alibaba/testable/core/util/InvokeRecordUtil"; private static final String METHOD_RECORD_MOCK_INVOKE = "recordMockInvoke"; private static final String SIGNATURE_INVOKE_RECORDER_METHOD = "([Ljava/lang/Object;ZZ)V"; @@ -29,9 +30,6 @@ public class MockClassHandler extends BaseClassHandler { @Override protected void transform(ClassNode cn) { - if (wasTransformed(cn, REF_INSTANCE, ClassUtil.toByteCodeClassName(mockClassName))) { - return; - } addGetInstanceMethod(cn); for (MethodNode mn : cn.methods) { if (isMockMethod(mn)) { @@ -39,25 +37,41 @@ public class MockClassHandler extends BaseClassHandler { mn.access &= ~ACC_PROTECTED; mn.access |= ACC_PUBLIC; injectInvokeRecorder(mn); + handleInstruction(cn, mn); } } } + @Override + protected String getTestCaseMark() { + String mockClass = Thread.currentThread().getStackTrace()[InvokeRecordUtil.INDEX_OF_TEST_CLASS].getClassName(); + // TODO: temporary used + String testClass = mockClass.substring(0, mockClass.length() - 5); + String testCaseName = TestableUtil.currentTestCaseName(testClass); + return testClass + "::" + testCaseName; + } + + private void handleInstruction(ClassNode cn, MethodNode mn) { + AbstractInsnNode[] instructions = mn.instructions.toArray(); + for (int i = 0; i < instructions.length; i++) { + instructions = handleTestableUtil(cn, mn, instructions, i); + } + } + private void addGetInstanceMethod(ClassNode cn) { - MethodNode getInstanceMethod = new MethodNode(ACC_PUBLIC | ACC_STATIC, REF_GET_INSTANCE, + MethodNode getInstanceMethod = new MethodNode(ACC_PUBLIC | ACC_STATIC, GET_TESTABLE_REF, VOID_ARGS + ClassUtil.toByteCodeClassName(mockClassName), null, null); InsnList il = new InsnList(); - il.add(new FieldInsnNode(GETSTATIC, mockClassName, REF_INSTANCE, - ClassUtil.toByteCodeClassName(mockClassName))); + il.add(new FieldInsnNode(GETSTATIC, mockClassName, TESTABLE_REF, 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(new FieldInsnNode(PUTSTATIC, mockClassName, TESTABLE_REF, 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 FieldInsnNode(GETSTATIC, mockClassName, TESTABLE_REF, ClassUtil.toByteCodeClassName(mockClassName))); il.add(new InsnNode(ARETURN)); getInstanceMethod.instructions = il; getInstanceMethod.maxStack = 2; 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 f9fb468..00c5f22 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 @@ -20,8 +20,6 @@ import static com.alibaba.testable.agent.constant.ConstPool.CONSTRUCTOR; */ public class SourceClassHandler extends BaseClassHandler { - private static final String TESTABLE_MARK_FIELD = "__testable"; - private final List injectMethods; private final Set invokeOps = new HashSet() {{ add(Opcodes.INVOKEVIRTUAL); @@ -41,9 +39,6 @@ public class SourceClassHandler extends BaseClassHandler { */ @Override protected void transform(ClassNode cn) { - if (wasTransformed(cn, TESTABLE_MARK_FIELD, ClassUtil.toByteCodeClassName(mockClassName))) { - return; - } Set memberInjectMethods = new HashSet(); Set newOperatorInjectMethods = new HashSet(); for (MethodInfo im : injectMethods) { @@ -209,10 +204,9 @@ public class SourceClassHandler extends BaseClassHandler { newOperatorInjectMethodName); String classType = ((TypeInsnNode)instructions[start]).desc; String constructorDesc = ((MethodInsnNode)instructions[end]).desc; - String testClassName = ClassUtil.getTestClassName(cn.name); mn.instructions.insertBefore(instructions[start], new MethodInsnNode(INVOKESTATIC, mockClassName, - REF_GET_INSTANCE, VOID_ARGS + ClassUtil.toByteCodeClassName(mockClassName), false)); - mn.instructions.insertBefore(instructions[end], new MethodInsnNode(INVOKEVIRTUAL, testClassName, + GET_TESTABLE_REF, VOID_ARGS + ClassUtil.toByteCodeClassName(mockClassName), false)); + mn.instructions.insertBefore(instructions[end], new MethodInsnNode(INVOKEVIRTUAL, mockClassName, newOperatorInjectMethodName, getConstructorInjectDesc(constructorDesc, classType), false)); mn.instructions.remove(instructions[start]); mn.instructions.remove(instructions[start + 1]); @@ -240,9 +234,8 @@ public class SourceClassHandler extends BaseClassHandler { LogUtil.diagnose(" Line %d, mock method \"%s\" used", getLineNum(instructions, start), mockMethod.getMockName()); boolean shouldAppendTypeParameter = !mockMethod.getDesc().equals(mockMethod.getMockDesc()); - String testClassName = ClassUtil.getTestClassName(cn.name); mn.instructions.insertBefore(instructions[start], new MethodInsnNode(INVOKESTATIC, mockClassName, - REF_GET_INSTANCE, VOID_ARGS + ClassUtil.toByteCodeClassName(mockClassName), false)); + GET_TESTABLE_REF, VOID_ARGS + ClassUtil.toByteCodeClassName(mockClassName), false)); if (Opcodes.INVOKESTATIC == opcode || isCompanionMethod(ownerClass, opcode)) { if (shouldAppendTypeParameter) { // append a null value if it was a static invoke or in kotlin companion class @@ -260,7 +253,7 @@ public class SourceClassHandler extends BaseClassHandler { } } // method with @MockMethod will be modified as public access, so INVOKEVIRTUAL is used - mn.instructions.insertBefore(instructions[end], new MethodInsnNode(INVOKEVIRTUAL, testClassName, + mn.instructions.insertBefore(instructions[end], new MethodInsnNode(INVOKEVIRTUAL, mockClassName, mockMethod.getMockName(), mockMethod.getMockDesc(), false)); mn.instructions.remove(instructions[end]); return new ModifiedInsnNodes(mn.instructions.toArray(), 1); 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 548c672..dd4ed15 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 @@ -1,37 +1,15 @@ package com.alibaba.testable.agent.handler; -import com.alibaba.testable.agent.constant.ConstPool; -import com.alibaba.testable.agent.tool.ImmutablePair; -import com.alibaba.testable.agent.util.AnnotationUtil; -import com.alibaba.testable.agent.util.ClassUtil; -import com.alibaba.testable.core.util.LogUtil; -import org.objectweb.asm.Type; -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; +import com.alibaba.testable.core.util.InvokeRecordUtil; +import com.alibaba.testable.core.util.TestableUtil; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodNode; /** * @author flin */ -public class TestClassHandler extends BaseClassHandler { - - private static final String CLASS_TESTABLE_TOOL = "com/alibaba/testable/core/tool/TestableTool"; - private static final String CLASS_TESTABLE_UTIL = "com/alibaba/testable/core/util/TestableUtil"; - private static final String CLASS_MOCK_CONTEXT = "com/alibaba/testable/agent/model/MockContext"; - private static final String REF_TESTABLE_CONTEXT = "_testableContextReference"; - private static final String FIELD_TEST_CASE = "TEST_CASE"; - private static final String FIELD_SOURCE_METHOD = "SOURCE_METHOD"; - private static final String FIELD_MOCK_CONTEXT = "MOCK_CONTEXT"; - private static final String FIELD_PARAMETERS = "parameters"; - private static final String METHOD_CURRENT_TEST_CASE_NAME = "currentTestCaseName"; - private static final String METHOD_CURRENT_SOURCE_METHOD_NAME = "currentSourceMethodName"; - private static final String SIGNATURE_CURRENT_TEST_CASE_NAME = "(Ljava/lang/String;)Ljava/lang/String;"; - private static final String SIGNATURE_CURRENT_SOURCE_METHOD_NAME = "()Ljava/lang/String;"; - private static final String SIGNATURE_PARAMETERS = "Ljava/util/Map;"; +public class TestClassHandler extends BaseClassWithContextHandler { public TestClassHandler(String mockClassName) { this.mockClassName = mockClassName; @@ -43,69 +21,23 @@ public class TestClassHandler extends BaseClassHandler { */ @Override protected void transform(ClassNode cn) { - if (wasTransformed(cn, REF_TESTABLE_CONTEXT, ClassUtil.toByteCodeClassName(mockClassName))) { - return; - } for (MethodNode mn : cn.methods) { - if (mn.name.equals(ConstPool.CONSTRUCTOR)) { - initMockContextReference(cn, mn); - } else { - handleInstruction(cn, mn); - } + handleInstruction(cn, mn); } } - private void initMockContextReference(ClassNode cn, MethodNode mn) { - 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, 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); - mn.maxStack++; + @Override + protected String getTestCaseMark() { + String testClass = Thread.currentThread().getStackTrace()[InvokeRecordUtil.INDEX_OF_TEST_CLASS].getClassName(); + String testCaseName = TestableUtil.currentTestCaseName(testClass); + return testClass + "::" + testCaseName; } private void handleInstruction(ClassNode cn, MethodNode mn) { AbstractInsnNode[] instructions = mn.instructions.toArray(); - int i = 0; - do { - if (instructions[i].getOpcode() == GETSTATIC) { - FieldInsnNode fieldInsnNode = (FieldInsnNode)instructions[i]; - if (isTestableUtilField(fieldInsnNode)) { - instructions = replaceTestableUtilField(cn, mn, instructions, fieldInsnNode.name, i); - } - } - i++; - } while (i < instructions.length); - } - - private boolean isTestableUtilField(FieldInsnNode fieldInsnNode) { - return fieldInsnNode.owner.equals(CLASS_TESTABLE_TOOL) && - (fieldInsnNode.name.equals(FIELD_TEST_CASE) || fieldInsnNode.name.equals(FIELD_SOURCE_METHOD) || - fieldInsnNode.name.equals(FIELD_MOCK_CONTEXT)); - } - - private AbstractInsnNode[] replaceTestableUtilField(ClassNode cn, MethodNode mn, AbstractInsnNode[] instructions, - String fieldName, int pos) { - InsnList il = new InsnList(); - if (FIELD_TEST_CASE.equals(fieldName)) { - il.add(new LdcInsnNode(ClassUtil.toDotSeparatedName(cn.name))); - il.add(new MethodInsnNode(INVOKESTATIC, CLASS_TESTABLE_UTIL, METHOD_CURRENT_TEST_CASE_NAME, - SIGNATURE_CURRENT_TEST_CASE_NAME, false)); - } else if (FIELD_SOURCE_METHOD.equals(fieldName)) { - il.add(new MethodInsnNode(INVOKESTATIC, CLASS_TESTABLE_UTIL, METHOD_CURRENT_SOURCE_METHOD_NAME, - SIGNATURE_CURRENT_SOURCE_METHOD_NAME, false)); - } else if (FIELD_MOCK_CONTEXT.equals(fieldName)) { - il.add(new FieldInsnNode(GETSTATIC, cn.name, REF_TESTABLE_CONTEXT, - ClassUtil.toByteCodeClassName(CLASS_MOCK_CONTEXT))); - il.add(new FieldInsnNode(GETFIELD, CLASS_MOCK_CONTEXT, FIELD_PARAMETERS, SIGNATURE_PARAMETERS)); + for (int i = 0; i < instructions.length; i++) { + instructions = handleTestableUtil(cn, mn, instructions, i); } - if (il.size() > 0) { - mn.instructions.insert(instructions[pos], il); - mn.instructions.remove(instructions[pos]); - } - return mn.instructions.toArray(); } } diff --git a/testable-agent/src/main/java/com/alibaba/testable/agent/model/MockContext.java b/testable-agent/src/main/java/com/alibaba/testable/agent/model/MockContext.java index 8cc1418..1c21a64 100644 --- a/testable-agent/src/main/java/com/alibaba/testable/agent/model/MockContext.java +++ b/testable-agent/src/main/java/com/alibaba/testable/agent/model/MockContext.java @@ -8,6 +8,9 @@ import java.util.Map; */ public class MockContext { - public Map parameters = new HashMap(); + /** + * Mock method name → ( key → value ) + */ + public static final Map> parameters = new HashMap>(); }