package org.testng.eclipse.ui.conversion;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.IExtendedModifier;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.ImportDeclaration;
import org.eclipse.jdt.core.dom.MemberValuePair;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.NormalAnnotation;
import org.eclipse.jdt.core.dom.SimpleType;
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
import org.eclipse.jdt.core.dom.SuperConstructorInvocation;
import org.eclipse.jdt.core.dom.SuperMethodInvocation;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.testng.AssertJUnit;
import junit.framework.Assert;
/**
* This visitor stores all the interesting things in a JUnit class:
* - JUnit imports
* - "extends TestCase" declaration
* - Methods that start with test
* - setUp and tearDown
* - ...
*
* Created on Aug 8, 2005
* @author Cedric Beust <cedric@beust.com>
*/
public class JUnitVisitor extends Visitor {
private List<MethodDeclaration> m_testMethods = new ArrayList<>();
private List<MethodDeclaration> m_disabledTestMethods = new ArrayList<>();
private List<MethodDeclaration> m_beforeMethods = new ArrayList<>();
private List<MethodDeclaration> m_afterMethods = new ArrayList<>();
private List<MethodDeclaration> m_beforeClasses = new ArrayList<>();
private List<MethodDeclaration> m_afterClasses = new ArrayList<>();
private MethodDeclaration m_suite = null;
// Parent classes
private SimpleType m_testCase = null;
private boolean m_isTestSuite = false;
// The JUnit imports found on this class
private List<ImportDeclaration> m_junitImports = new ArrayList<>();
// Static imports for assert methods
private Set<String> m_assertStaticImports = new HashSet<>();
// List of all the methods that have @Test(expected) or @Test(timeout)
private Map<MemberValuePair, String> m_testsWithExpected = new HashMap<>();
// The position and length of all the Assert references that are not statically
// imported
private Set<MethodInvocation> m_asserts = new HashSet<>();
// The position and length of all the fail() calls
private Set<MethodInvocation> m_fails = new HashSet<>();
// True if there are test methods (if they are annotated with @Test, they won't
// show up in m_testMethods).
private boolean m_hasTestMethods = false;
// Nodes that need to be removed by the refactoring
private List<ASTNode> m_nodesToRemove = new ArrayList<>();
private SuperConstructorInvocation m_superConstructorInvocation;
private String m_className;
private SingleMemberAnnotation m_runWithParameterized;
private MethodDeclaration m_parametersMethod;
private TypeDeclaration m_type;
private boolean m_hasDefaultConstructor = false;
private Map<MethodDeclaration, Annotation> m_ignoredMethods = new HashMap<>();
// The list of methods that are present on JUnit's Assert class
private static Set<String> m_assertMethods = new HashSet<>();
static {
for (Method m : Assert.class.getDeclaredMethods()) {
m_assertMethods.add(m.getName());
}
// Also add a few methods from the JUnit4 Assert class
m_assertMethods.add("assertArrayEquals");
}
@Override
public boolean visit(ImportDeclaration id) {
Name simpleName = id.getName();
String name = simpleName.getFullyQualifiedName();
if (name.indexOf("junit") != -1) {
int ind = simpleName.toString().indexOf("assert");
if (id.isStatic() && ind > 0) {
m_assertStaticImports.add(simpleName.toString().substring(ind));
}
m_junitImports.add(id);
}
return super.visit(id);
}
@Override
public boolean visit(SuperMethodInvocation smi) {
String name = smi.getName().toString();
// Only remove the call to super if the class extends TestCase directly
if (m_testCase != null && ("setUp".equals(name) || "tearDown".equals(name))) {
m_nodesToRemove.add(smi.getParent());
}
return super.visit(smi);
}
/**
* Remember if we find a constructor that calls super(String).
*/
@Override
public boolean visit(SuperConstructorInvocation sci) {
// Only remove the call to super if the class extends TestCase directly
if (m_testCase != null) {
List args = sci.arguments();
if (args.size() == 1) {
Expression arg = (Expression) args.get(0);
ITypeBinding binding = arg.resolveTypeBinding();
if (binding != null && String.class.getName().equals(binding.getBinaryName())) {
m_superConstructorInvocation = sci;
}
}
}
return super.visit(sci);
}
public SuperConstructorInvocation getSuperConstructorInvocation() {
return m_superConstructorInvocation;
}
@Override
public boolean visit(MethodDeclaration md) {
String methodName = md.getName().getFullyQualifiedName();
if (methodName.equals("setUp") || hasAnnotation(md, "Before")) {
m_beforeMethods.add(md);
} else if (methodName.equals("tearDown") || hasAnnotation(md, "After")) {
m_afterMethods.add(md);
} else if (methodName.equals("suite")) {
m_suite = md;
} else if (methodName.equals(m_type.getName().toString())) {
// A constructor
if (md.parameters().size() == 0) {
m_hasDefaultConstructor = true;
}
} else if (hasAnnotation(md, "Parameters")) {
m_parametersMethod = md;
} else if (hasAnnotation(md, "BeforeClass")) {
m_beforeClasses.add(md);
} else if (hasAnnotation(md, "AfterClass")) {
m_afterClasses.add(md);
} else if (hasAnnotation(md, "Ignore")) {
m_ignoredMethods.put(md, getAnnotation(md, "Ignore"));
} else if (! hasAnnotation(md, "Test")) {
// Public methods that start with "test" are tests.
// Methods that start with "_test" or private test methods that start with "test" are disabled
boolean isPrivate = (md.getModifiers() & Modifier.PRIVATE) != 0;
if (methodName.startsWith("test") && ! isPrivate) {
m_testMethods.add(md);
} else if (methodName.startsWith("_test") || (methodName.startsWith("test") && isPrivate)) {
m_disabledTestMethods.add(md);
}
} else if (hasAnnotation(md, "Test")) {
m_hasTestMethods = true; // to make sure we import org.testng.annotations.Test
MemberValuePair mvp = getAttribute(md, "expected");
if (mvp != null) {
m_testsWithExpected.put(mvp, "expectedExceptions");
}
mvp = getAttribute(md, "timeout");
if (mvp != null) {
m_testsWithExpected.put(mvp, "timeOut");
}
}
return super.visit(md);
}
/**
* @return true if the given method is annotated @Test(expected = ...)
*/
private MemberValuePair getAttribute(MethodDeclaration md, String attribute) {
@SuppressWarnings("unchecked")
List<IExtendedModifier> modifiers = md.modifiers();
for (IExtendedModifier m : modifiers) {
if (m.isAnnotation()) {
Annotation a = (Annotation) m;
if ("Test".equals(a.getTypeName().toString()) && a instanceof NormalAnnotation) {
NormalAnnotation na = (NormalAnnotation) a;
for (Object o : na.values()) {
MemberValuePair mvp = (MemberValuePair) o;
if (mvp.getName().toString().equals(attribute)) return mvp;
}
}
}
}
return null;
}
/**
* Record whether this type declaration is a TestCase or a TestSuite.
*/
@Override
public boolean visit(TypeDeclaration td) {
m_className = td.getName().toString();
m_type = td;
//
// Is this class annotated with @RunWith(Parameterized.class)?
//
List<IExtendedModifier> modifiers = td.modifiers();
for (IExtendedModifier m : modifiers) {
if (m.isAnnotation() && m instanceof SingleMemberAnnotation) {
SingleMemberAnnotation a = (SingleMemberAnnotation) m;
if ("RunWith".equals(a.getTypeName().toString()) &&
"Parameterized.class".equals(a.getValue().toString())) {
m_runWithParameterized = a;
}
}
}
//
// Is the class a direct subclass of TestCase?
//
Type superClass = td.getSuperclassType();
if (superClass instanceof SimpleType) {
SimpleType st = (SimpleType) superClass;
if ("TestCase".equals(st.getName().getFullyQualifiedName())) {
m_testCase = st;
}
}
// Is the class a subclass of TestSuite?
if (superClass != null) {
ITypeBinding binding = superClass.resolveBinding();
while (binding != null) {
if ("TestSuite".equals(binding.getName())) {
m_isTestSuite = true;
break;
} else {
binding = binding.getSuperclass();
}
}
}
return super.visit(td);
}
public SingleMemberAnnotation getRunWithParameterized() {
return m_runWithParameterized;
}
public MethodDeclaration getParametersMethod() {
return m_parametersMethod;
}
/**
* Find occurrences of "Assert.xxx", which need to be replaced with "AssertJUnit.xxx".
*/
@Override
public boolean visit(MethodInvocation node) {
Expression exp = node.getExpression();
String method = node.getName().toString();
if ((exp != null && "Assert".equals(exp.toString())) || method.startsWith("assert")) {
// Method prefixed with "Assert."
if (belongsToAssertJUnit(node) &&
! m_assertStaticImports.contains(node.getName().toString())) {
m_asserts.add(node);
}
} else if ("fail".equals(method)) {
// assert or fail not prefixed with "Assert."
if (belongsToAssertJUnit(node)) m_fails.add(node);
}
return super.visit(node);
}
// Class internal names, according to
// http://download.oracle.com/javase/1.4.2/docs/api/java/lang/Class.html#getName()
private static Map<String, Class> BINARY_CLASS_NAMES = new HashMap<String, Class>() {{
put("B", byte.class);
put("C", char.class);
put("D", double.class);
put("F", float.class);
put("I", int.class);
put("J", long.class);
put("L", Class.class);
put("S", short.class);
put("Z", boolean.class);
put("[", Object[].class);
}};
private Class getBinaryClassName(String binaryName) {
return BINARY_CLASS_NAMES.get(binaryName);
}
/**
* @return true if this method is defined on the AssertJUnit class.
*/
private boolean belongsToAssertJUnit(MethodInvocation method) {
if (! m_assertMethods.contains(method.getName().toString())) return false;
List<Expression> arguments = method.arguments();
List<Class> types = new ArrayList<>();
for (Expression e : arguments) {
ITypeBinding binding = e.resolveTypeBinding();
// Early abort if a binding fails
if (binding == null) {
return true;
}
Class c = bindingToClass(binding);
types.add(c);
}
boolean result = false;
adjustForOverloading(types);
// We need to correct types[1] and types[2] so they match here, in order to
// emulate type conversions (for example, (int, long) should become (long, long)
// Try to find a method with the exact signature. This can fail for a few reasons
// (such as not being able to resolve the binary name), in which case I should try
// to fall back on a simple name search
try {
Object m = AssertJUnit.class.getMethod(method.getName().getFullyQualifiedName(),
types.toArray(new Class[types.size()]));
result = true;
} catch (SecurityException e1) {
// e1.printStackTrace();
} catch (NoSuchMethodException e1) {
// e1.printStackTrace();
}
if (! result && (arguments.size() == 2 || arguments.size() == 3)) {
// An assert with two or three parameters will match assertTrue(Object, Object)
result = true;
}
return result;
}
/**
* Modify the list of parameter types to try to match how the compiler will
* resolve the type conversion. For example, an invocation of assert(..., int, long)
* will probably end up calling assert(..., long, long).
*/
private void adjustForOverloading(List<Class> types) {
if (types.size() > 2) {
Class t2 = types.get(1);
Class t3 = types.get(2);
if ((t2 == long.class && t3 == int.class) || (t3 == long.class && t2 == int.class)) {
types.set(1, long.class);
types.set(2, long.class);
}
}
}
/**
* Use heuristics to try to find the right class for this binding.
*/
private Class bindingToClass(ITypeBinding binding) {
Class result = getBinaryClassName(binding.getBinaryName());
if (result == null) {
try {
result = Class.forName(binding.getQualifiedName());
} catch (ClassNotFoundException e) {
// ignore
}
}
if (result == null) result = Object.class;
return result;
}
public Set<MethodInvocation> getAsserts() {
return m_asserts;
}
private static void ppp(String s) {
System.out.println("[JUnitVisitor] " + s);
Assert.assertTrue(true);
}
public Collection<MethodDeclaration> getBeforeMethods() {
return m_beforeMethods;
}
public Collection<MethodDeclaration> getAfterMethods() {
return m_afterMethods;
}
public Collection<MethodDeclaration> getBeforeClasses() {
return m_beforeClasses;
}
public Collection<MethodDeclaration> getAfterClasses() {
return m_afterClasses;
}
public MethodDeclaration getSuite() {
return m_suite;
}
public void setSuite(MethodDeclaration suite) {
m_suite = suite;
}
public Collection<MethodDeclaration> getTestMethods() {
return m_testMethods;
}
public Collection<MethodDeclaration> getDisabledTestMethods() {
return m_disabledTestMethods;
}
public boolean hasTestMethods() {
return m_hasTestMethods || m_testMethods.size() > 0 || m_disabledTestMethods.size() > 0;
}
public void setTestMethods(List<MethodDeclaration> testMethods) {
m_testMethods = testMethods;
}
public SimpleType getTestCase() {
return m_testCase;
}
public List<ImportDeclaration> getJUnitImports() {
return m_junitImports;
}
public Set<String> getStaticImports() {
return m_assertStaticImports;
}
public boolean hasAsserts() {
return m_asserts.size() > 0;
}
public Set<MethodInvocation> getFails() {
return m_fails;
}
public boolean hasFail() {
return m_fails.size() > 0;
}
/**
* All the @Test annotated methods that have attributes that need to be replaced.
*/
public Map<MemberValuePair, String> getTestsWithExpected() {
return m_testsWithExpected;
}
@Override
public String toString() {
return "[JUnitVisitor for class " + m_className + "]";
}
public List<ASTNode> getNodesToRemove() {
return m_nodesToRemove;
}
/**
* @return whether this file needs to be changed in order to be converted to TestNG.
*/
public boolean needsConversion() {
if (m_isTestSuite) {
return false;
}
if (m_hasTestMethods || getTestMethods().size() > 0 || getDisabledTestMethods().size() > 0 ||
getBeforeMethods().size() > 0 || getAfterMethods().size() > 0 || m_suite != null) {
return true;
}
return false;
}
public TypeDeclaration getType() {
return m_type;
}
public boolean hasDefaultConstructor() {
return m_hasDefaultConstructor;
}
public Map<MethodDeclaration, Annotation> getIgnoredMethods() {
return m_ignoredMethods;
}
}