add substitutions to global inject method pool

This commit is contained in:
金戟 2020-05-19 21:18:08 +08:00
parent 24a0cb32b2
commit 14659f332d
3 changed files with 183 additions and 104 deletions

View File

@ -1,13 +1,10 @@
package com.alibaba.testable.generator;
import com.alibaba.testable.util.ConstPool;
import com.squareup.javapoet.*;
import com.alibaba.testable.model.TestableContext;
import javax.lang.model.element.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
/**
* Generate global n.e class code
@ -16,55 +13,24 @@ import java.util.Map;
*/
public class StaticNewClassGenerator {
private final TestableContext cx;
public StaticNewClassGenerator(TestableContext cx) {
this.cx = cx;
}
public String fetch() {
return JavaFile.builder(ConstPool.SN_PKG,
TypeSpec.classBuilder(ConstPool.SN_CLS)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addField(buildStaticPoolField())
.addMethod(buildStaticNewMethod())
.build())
.build().toString();
}
private FieldSpec buildStaticPoolField() {
return FieldSpec.builder(ParameterizedTypeName.get(Map.class, Class.class, Object.class), "pool", Modifier.PUBLIC)
.addModifiers(Modifier.STATIC)
.initializer("new $T<>()", HashMap.class)
.build();
}
private MethodSpec buildStaticNewMethod() {
TypeVariableName typeVariable = TypeVariableName.get("T");
MethodSpec.Builder builder = MethodSpec.methodBuilder(ConstPool.SN_METHOD)
.addModifiers(Modifier.PUBLIC).addModifiers(Modifier.STATIC)
.addTypeVariable(typeVariable)
.varargs(true)
.addParameter(ParameterizedTypeName.get(ClassName.get(Class.class), typeVariable), "type")
.addParameter(ArrayTypeName.of(Object.class), "args")
.returns(typeVariable);
addStaticNewMethodStatement(builder);
return builder.build();
}
private void addStaticNewMethodStatement(MethodSpec.Builder builder) {
builder.addStatement("$T<$T> pts = new $T<>()", List.class, Class.class, ArrayList.class)
.beginControlFlow("for (Object o : args)")
.addStatement("pts.add(o.getClass())")
.endControlFlow()
.beginControlFlow("if (!pool.isEmpty())")
.beginControlFlow("try")
.addStatement("T obj = (T)pool.get(type)")
.beginControlFlow("if (obj != null)")
.addStatement("return obj")
.endControlFlow()
.nextControlFlow("catch (Exception e)")
.endControlFlow()
.endControlFlow()
.beginControlFlow("try")
.addStatement("return type.getConstructor(pts.toArray(new Class[0])).newInstance(args)")
.nextControlFlow("catch (Exception e)")
.addStatement("return null")
.endControlFlow();
ClassLoader classLoader = this.getClass().getClassLoader();
File file = new File(classLoader.getResource("e.java").getFile());
if (!file.exists()) {
cx.logger.error("Failed to fetch testable new stand-in.");
}
try {
return new String(Files.readAllBytes(file.toPath()));
} catch (IOException e) {
cx.logger.error("Failed to generate testable new stand-in.");
return "";
}
}
}

View File

@ -9,6 +9,7 @@ import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.TreeTranslator;
import com.sun.tools.javac.util.*;
import com.sun.tools.javac.tree.JCTree.*;
import java.lang.reflect.Modifier;
@ -23,11 +24,15 @@ public class TestableClassTestRoleTranslator extends TreeTranslator {
private static final String ANNOTATION_JUNIT5_SETUP = "org.junit.jupiter.api.BeforeEach";
private static final String ANNOTATION_JUNIT5_TEST = "org.junit.jupiter.api.Test";
private final TestableContext cx;
private String sourceClassName;
private String sourceClassName = "";
private ListBuffer<Name> sourceClassIns = new ListBuffer();
private List<String> stubbornFields = List.nil();
private ListBuffer<Pair<Type, Pair<Name, List<Type>>>> injectMethods = new ListBuffer<>();
private String testSetupMethodName;
/**
* MethodName -> (ResultType -> ParameterTypes)
*/
private ListBuffer<Pair<Name, Pair<JCExpression, List<JCExpression>>>> injectMethods = new ListBuffer<>();
private String testSetupMethodName = "";
private TestLibType testLibType = TestLibType.JUnit4;
public TestableClassTestRoleTranslator(String pkgName, String className, TestableContext cx) {
@ -44,29 +49,39 @@ public class TestableClassTestRoleTranslator extends TreeTranslator {
}
@Override
public void visitVarDef(JCTree.JCVariableDecl jcVariableDecl) {
public void visitVarDef(JCVariableDecl jcVariableDecl) {
super.visitVarDef(jcVariableDecl);
if (((JCTree.JCIdent)jcVariableDecl.vartype).name.toString().equals(sourceClassName)) {
if (((JCIdent)jcVariableDecl.vartype).name.toString().equals(sourceClassName)) {
jcVariableDecl.vartype = getTestableClassIdent(jcVariableDecl.vartype);
sourceClassIns.add(jcVariableDecl.name);
}
}
@Override
public void visitNewClass(JCTree.JCNewClass jcNewClass) {
public void visitNewClass(JCNewClass jcNewClass) {
super.visitNewClass(jcNewClass);
if (((JCTree.JCIdent)jcNewClass.clazz).name.toString().equals(sourceClassName)) {
if (getSimpleClassName(jcNewClass).equals(sourceClassName)) {
jcNewClass.clazz = getTestableClassIdent(jcNewClass.clazz);
}
}
private String getSimpleClassName(JCNewClass jcNewClass) {
if (jcNewClass.clazz.getClass().equals(JCIdent.class)) {
return ((JCIdent)jcNewClass.clazz).name.toString();
} else if (jcNewClass.clazz.getClass().equals(JCFieldAccess.class)) {
return ((JCFieldAccess)jcNewClass.clazz).name.toString();
} else {
return "";
}
}
@Override
public void visitExec(JCTree.JCExpressionStatement jcExpressionStatement) {
if (jcExpressionStatement.expr.getClass().equals(JCTree.JCAssign.class) &&
isAssignStubbornField((JCTree.JCAssign)jcExpressionStatement.expr)) {
JCTree.JCAssign assign = (JCTree.JCAssign)jcExpressionStatement.expr;
public void visitExec(JCExpressionStatement jcExpressionStatement) {
if (jcExpressionStatement.expr.getClass().equals(JCAssign.class) &&
isAssignStubbornField((JCAssign)jcExpressionStatement.expr)) {
JCAssign assign = (JCAssign)jcExpressionStatement.expr;
// TODO: Use treeMaker.Apply() and treeMaker.Select()
TestableFieldAccess stubbornSetter = new TestableFieldAccess(((JCTree.JCFieldAccess)assign.lhs).selected,
TestableFieldAccess stubbornSetter = new TestableFieldAccess(((JCFieldAccess)assign.lhs).selected,
getStubbornSetterMethodName(assign), null);
jcExpressionStatement.expr = new TestableMethodInvocation(null, stubbornSetter,
com.sun.tools.javac.util.List.of(assign.rhs));
@ -75,15 +90,16 @@ public class TestableClassTestRoleTranslator extends TreeTranslator {
}
@Override
public void visitMethodDef(JCTree.JCMethodDecl jcMethodDecl) {
for (JCTree.JCAnnotation a : jcMethodDecl.mods.annotations) {
public void visitMethodDef(JCMethodDecl jcMethodDecl) {
for (JCAnnotation a : jcMethodDecl.mods.annotations) {
switch (a.type.tsym.toString()) {
case ANNOTATION_TESTABLE_INJECT:
ListBuffer<Type> args = new ListBuffer<>();
for (JCTree.JCVariableDecl p : jcMethodDecl.params) {
args.add(p.vartype.type);
ListBuffer<JCExpression> args = new ListBuffer<>();
for (JCVariableDecl p : jcMethodDecl.params) {
args.add(cx.treeMaker.Select(p.vartype, cx.names.fromString("class")));
}
injectMethods.add(Pair.of(jcMethodDecl.restype.type, Pair.of(jcMethodDecl.name, args.toList())));
JCExpression retType = cx.treeMaker.Select(jcMethodDecl.restype, cx.names.fromString("class"));
injectMethods.add(Pair.of(jcMethodDecl.name, Pair.of(retType, args.toList())));
break;
case ANNOTATION_JUNIT5_SETUP:
testSetupMethodName = jcMethodDecl.name.toString();
@ -99,35 +115,22 @@ public class TestableClassTestRoleTranslator extends TreeTranslator {
}
@Override
public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
public void visitClassDef(JCClassDecl jcClassDecl) {
super.visitClassDef(jcClassDecl);
ListBuffer<JCTree> ndefs = new ListBuffer<>();
ndefs.addAll(jcClassDecl.defs);
JCTree.JCModifiers mods = cx.treeMaker.Modifiers(Modifier.PUBLIC, makeAnnotations());
JCModifiers mods = cx.treeMaker.Modifiers(Modifier.PUBLIC, makeAnnotations(ANNOTATION_JUNIT5_SETUP));
ndefs.add(cx.treeMaker.MethodDef(mods, cx.names.fromString("testableSetup"),
cx.treeMaker.Type(new Type.JCVoidType()), List.<JCTree.JCTypeParameter>nil(),
List.<JCTree.JCVariableDecl>nil(), List.<JCTree.JCExpression>nil(), testableSetupBlock(), null));
cx.treeMaker.Type(new Type.JCVoidType()), List.<JCTypeParameter>nil(),
List.<JCVariableDecl>nil(), List.<JCExpression>nil(), testableSetupBlock(), null));
jcClassDecl.defs = ndefs.toList();
}
private List<JCTree.JCAnnotation> makeAnnotations() {
String[] elems = ANNOTATION_JUNIT5_TEST.split("\\.");
JCTree.JCExpression e = cx.treeMaker.Ident(cx.names.fromString(elems[0]));
for (int i = 1 ; i < elems.length ; i++) {
e = cx.treeMaker.Select(e, cx.names.fromString(elems[i]));
}
return List.of(cx.treeMaker.Annotation(e, List.<JCTree.JCExpression>nil()));
}
private JCTree.JCBlock testableSetupBlock() {
return cx.treeMaker.Block(0, List.<JCTree.JCStatement>nil());
}
/**
* For break point
*/
@Override
public void visitAssign(JCTree.JCAssign jcAssign) {
public void visitAssign(JCAssign jcAssign) {
super.visitAssign(jcAssign);
}
@ -135,14 +138,53 @@ public class TestableClassTestRoleTranslator extends TreeTranslator {
* For break point
*/
@Override
public void visitSelect(JCTree.JCFieldAccess jcFieldAccess) {
public void visitSelect(JCFieldAccess jcFieldAccess) {
super.visitSelect(jcFieldAccess);
}
private List<JCTree.JCAnnotation> removeAnnotation(
List<JCTree.JCAnnotation> annotations, String target) {
ListBuffer<JCTree.JCAnnotation> nb = new ListBuffer<>();
for (JCTree.JCAnnotation i : annotations) {
private List<JCAnnotation> makeAnnotations(String fullAnnotationName) {
JCExpression setupAnnotation = nameToExpression(fullAnnotationName);
return List.of(cx.treeMaker.Annotation(setupAnnotation, List.<JCExpression>nil()));
}
private JCExpression nameToExpression(String dotName) {
String[] nameParts = dotName.split("\\.");
JCExpression e = cx.treeMaker.Ident(cx.names.fromString(nameParts[0]));
for (int i = 1 ; i < nameParts.length ; i++) {
e = cx.treeMaker.Select(e, cx.names.fromString(nameParts[i]));
}
return e;
}
private JCBlock testableSetupBlock() {
ListBuffer<JCStatement> statements = new ListBuffer<>();
for (Pair<Name, Pair<JCExpression, List<JCExpression>>> m : injectMethods.toList()) {
JCExpression key = nameToExpression("n.e.k");
JCExpression classType = m.snd.fst;
JCExpression parameterTypes = cx.treeMaker.NewArray(cx.treeMaker.Ident(cx.names.fromString("Class")),
List.<JCExpression>nil(), m.snd.snd);
JCNewClass keyClass = cx.treeMaker.NewClass(null, List.<JCExpression>nil(), key,
List.of(classType, parameterTypes), null);
JCExpression value = nameToExpression("n.e.v");
JCExpression thisIns = cx.treeMaker.Ident(cx.names.fromString("this"));
JCExpression methodName = cx.treeMaker.Literal(m.fst.toString());
JCNewClass valClass = cx.treeMaker.NewClass(null, List.<JCExpression>nil(), value,
List.of(thisIns, methodName), null);
JCExpression addInjectMethod = nameToExpression("n.e.a");
JCMethodInvocation apply = cx.treeMaker.Apply(List.<JCExpression>nil(), addInjectMethod,
List.from(new JCExpression[] {keyClass, valClass}));
statements.append(cx.treeMaker.Exec(apply));
}
if (!testSetupMethodName.isEmpty()) {
statements.append(cx.treeMaker.Exec(cx.treeMaker.Apply(List.<JCExpression>nil(),
nameToExpression(testSetupMethodName), List.<JCExpression>nil())));
}
return cx.treeMaker.Block(0, statements.toList());
}
private List<JCAnnotation> removeAnnotation(List<JCAnnotation> annotations, String target) {
ListBuffer<JCAnnotation> nb = new ListBuffer<>();
for (JCAnnotation i : annotations) {
if (!i.type.tsym.toString().equals(target)) {
nb.add(i);
}
@ -150,19 +192,19 @@ public class TestableClassTestRoleTranslator extends TreeTranslator {
return nb.toList();
}
private Name getStubbornSetterMethodName(JCTree.JCAssign assign) {
String name = ((JCTree.JCFieldAccess)assign.lhs).name.toString() + ConstPool.TESTABLE_SET_METHOD_PREFIX;
private Name getStubbornSetterMethodName(JCAssign assign) {
String name = ((JCFieldAccess)assign.lhs).name.toString() + ConstPool.TESTABLE_SET_METHOD_PREFIX;
return cx.names.fromString(name);
}
private boolean isAssignStubbornField(JCTree.JCAssign expr) {
return expr.lhs.getClass().equals(JCTree.JCFieldAccess.class) &&
sourceClassIns.contains(((JCTree.JCIdent)((JCTree.JCFieldAccess)(expr).lhs).selected).name) &&
stubbornFields.contains(((JCTree.JCFieldAccess)(expr).lhs).name.toString());
private boolean isAssignStubbornField(JCAssign expr) {
return expr.lhs.getClass().equals(JCFieldAccess.class) &&
sourceClassIns.contains(((JCIdent)((JCFieldAccess)(expr).lhs).selected).name) &&
stubbornFields.contains(((JCFieldAccess)(expr).lhs).name.toString());
}
private JCTree.JCIdent getTestableClassIdent(JCTree.JCExpression clazz) {
Name className = ((JCTree.JCIdent)clazz).name;
private JCIdent getTestableClassIdent(JCExpression clazz) {
Name className = ((JCIdent)clazz).name;
return cx.treeMaker.Ident(cx.names.fromString(className + ConstPool.TESTABLE));
}

71
src/main/resources/e.java Normal file
View File

@ -0,0 +1,71 @@
package n;
import java.lang.Class;
import java.lang.Object;
import java.lang.reflect.Method;
import java.util.*;
public final class e {
public static class k {
public Class c; // target instance type
public Class[] a; // constructor parameter types
public k(Class c, Class[] a) {
this.c = c;
this.a = a;
}
@Override
public boolean equals(Object o) {
return o.getClass().equals(k.class) && c.equals(((k)o).c) && Arrays.equals(a, ((k)o).a);
}
@Override
public int hashCode() {
return 31 * c.hashCode() + Arrays.hashCode(a);
}
}
public static class v {
public Object o; // testable object
public String m; // method to create instance
public v(Object o, String m) {
this.o = o;
this.m = m;
}
}
private static Map<k, v> pool = new HashMap<>();
public static void a(k ki, v vi) {
pool.put(ki, vi);
}
public static <T> T w(Class<T> ct, Object... as) {
Class[] cs = new Class[as.length / 2];
Object[] ps = new Object[as.length / 2];
for (int i = 0; i < cs.length; i++) {
cs[i] = (Class)as[i];
}
System.arraycopy(as, ps.length, ps, 0, ps.length);
if (!pool.isEmpty()) {
try {
v p = pool.get(new k(ct, cs));
if (p != null) {
Method m = p.o.getClass().getDeclaredMethod(p.m, cs);
m.setAccessible(true);
return (T)m.invoke(p.o, ps);
}
} catch (Exception e) {
return null;
}
}
try {
return ct.getConstructor(cs).newInstance(ps);
} catch (Exception e) {
return null;
}
}
}