/*******************************************************************************
* Copyright (c) 2012 Pivotal Software, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal Software, Inc. - initial API and implementation
*******************************************************************************/
package org.grails.ide.eclipse.core.junit;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import org.codehaus.groovy.ast.AnnotationNode;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.ModuleNode;
import org.codehaus.jdt.groovy.model.GroovyCompilationUnit;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.IRegion;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.search.IJavaSearchConstants;
import org.eclipse.jdt.core.search.IJavaSearchScope;
import org.eclipse.jdt.core.search.SearchEngine;
import org.eclipse.jdt.core.search.SearchMatch;
import org.eclipse.jdt.core.search.SearchParticipant;
import org.eclipse.jdt.core.search.SearchPattern;
import org.eclipse.jdt.core.search.SearchRequestor;
import org.eclipse.jdt.internal.junit.JUnitMessages;
import org.eclipse.jdt.internal.junit.launcher.ITestFinder;
import org.eclipse.jdt.internal.junit.util.CoreTestSearchEngine;
import org.grails.ide.eclipse.core.GrailsCoreActivator;
import org.grails.ide.eclipse.core.internal.GrailsNature;
import org.grails.ide.eclipse.core.model.GrailsVersion;
/**
* Tests related to JUnit integration (Run as >> Junit test) wotking with Grails 2.0.
*
* @author Kris De Volder
*
* @since 2.9
*/
public class Grails20AwareTestFinder implements ITestFinder {
public static final String TEST_FOR_ANNOT_NAME = "grails.test.mixin.TestFor";
private static boolean DEBUG = false; //Platform.getLocation().toString().contains("kdvolder");
private static void debug(Object msg) {
if (DEBUG) {
System.out.println(msg);
}
}
private ITestFinder wrappee;
public Grails20AwareTestFinder(ITestFinder finder) {
this.wrappee = finder;
}
//ITestFinder implementation
public void findTestsInContainer(IJavaElement element, Set result, IProgressMonitor pm) throws CoreException {
if (enableFor(element)) {
pm.beginTask("Searching for tests in "+element.getElementName(), 1);
try {
debug(">>>findTestsInContainer "+element.getElementName());
if (element instanceof IType) {
if (isGrail20Test((IType) element)) {
result.add(element);
}
} else {
searchForTests(element, result, new SubProgressMonitor(pm, 1));
}
debug("<<<findTestsInContainer "+element);
} finally {
pm.done();
}
wrappee.findTestsInContainer(element, result, pm);
//We no longer filter the results. This means we may include tests that won't run correctly with
//the 'naked' eclipse test runner. But users seem to prefer that over the filtering approach.
//See https://issuetracker.springsource.com/browse/STS-2481
//and https://issuetracker.springsource.com/browse/STS-2467
//removeNonUnitTests(result);
} else {
wrappee.findTestsInContainer(element, result, pm);
}
}
private void removeNonUnitTests(Set result) {
Iterator<Object> iter = result.iterator();
while (iter.hasNext()) {
Object o = iter.next();
if (o instanceof IType) { //Shouldn't be anything else than ITypes... but we check just to be sure.
IType type = (IType) o;
IPackageFragmentRoot pfr = (IPackageFragmentRoot) type.getAncestor(IJavaElement.PACKAGE_FRAGMENT_ROOT);
if (pfr!=null && isUnitTestPackageFragement(pfr)) {
//Keep it
} else {
iter.remove();
}
}
}
}
private void searchForTests(IJavaElement element, final Set result, IProgressMonitor pm) throws CoreException {
//Loosely based on a copy of org.eclipse.jdt.internal.junit.launcher.JUnit4TestFinder.findTestsInContainer(IJavaElement, Set, IProgressMonitor)
//Modifed to search just for Grails test classes marked by @TestFor annotations
try {
pm.beginTask(JUnitMessages.JUnit4TestFinder_searching_description, 4);
IRegion region= CoreTestSearchEngine.getRegion(element);
IJavaSearchScope scope= SearchEngine.createJavaSearchScope(region.getElements(), IJavaSearchScope.SOURCES);
int matchRule= SearchPattern.R_EXACT_MATCH | SearchPattern.R_CASE_SENSITIVE;
SearchPattern testForPattern= SearchPattern.createPattern(TEST_FOR_ANNOT_NAME, IJavaSearchConstants.ANNOTATION_TYPE, IJavaSearchConstants.ANNOTATION_TYPE_REFERENCE, matchRule);
SearchPattern testPattern= SearchPattern.createPattern("*Tests", IJavaSearchConstants.TYPE, IJavaSearchConstants.DECLARATIONS, matchRule);
SearchPattern theSearchPattern = SearchPattern.createOrPattern(testForPattern, testPattern);
SearchParticipant[] searchParticipants = new SearchParticipant[] { SearchEngine.getDefaultSearchParticipant() };
SearchRequestor requestor= new SearchRequestor() {
@Override
public void acceptSearchMatch(SearchMatch match) throws CoreException {
if (match.getAccuracy() == SearchMatch.A_ACCURATE && !match.isInsideDocComment()) {
Object element= match.getElement();
if (element instanceof IType && isGrail20Test((IType) element)) {
result.add(element);
}
}
}
};
new SearchEngine().search(theSearchPattern, searchParticipants, scope, requestor, new SubProgressMonitor(pm, 2));
} finally {
pm.done();
}
}
// private void findTestsIn(GroovyCompilationUnit element, Set result, IProgressMonitor pm) {
// GroovyProjectFacade gproj = new GroovyProjectFacade(element);
// List<ClassNode> types = element.getModuleNode().getClasses();
// if (types!=null) {
// for (ClassNode classNode : types) {
// if (isGrailsTest(classNode)) {
// result.add(o);
// }
// }
// }
// }
// private boolean isGrailsTest(ClassNode classNode) {
// List<AnnotationNode> annotations = classNode.getAnnotations();
// for (AnnotationNode a : annotations) {
// ClassNode cls = a.getClassNode();
// }
// return false;
// }
//ITestFinder implementation
public boolean isTest(IType type) throws CoreException {
debug("isTest? "+type.getElementName());
boolean result = false;
if (enableFor(type)) {
debug("isTest? "+type.getElementName());
//code specific for Grails 2.0 projects
result = isGrail20Test(type);
if (result) {
return result;
} else {
//We now always delegate the false case to wrappee. This means that we include integration and functional tests.
//While those tests may not actually run correctly with the 'naked' eclipse test runner. Some users
//are finding ways to make this work for them and get upset when it is no longer possible.
//See https://issuetracker.springsource.com/browse/STS-2481
//and https://issuetracker.springsource.com/browse/STS-2467
return wrappee.isTest(type);
}
} else {
return wrappee.isTest(type);
}
}
private boolean isGrail20Test(IType type) {
debug("isGrailsTest "+type);
try {
IPackageFragmentRoot pfr = (IPackageFragmentRoot) type.getAncestor(IJavaElement.PACKAGE_FRAGMENT_ROOT);
if (pfr!=null) {
// if (isUnitTestPackageFragement(pfr)) {
ICompilationUnit cu = type.getCompilationUnit();
if (cu instanceof GroovyCompilationUnit) {
GroovyCompilationUnit gcu = (GroovyCompilationUnit) cu;
ModuleNode module = gcu.getModuleNode();
if (module != null) {
List<ClassNode> classes = module.getClasses();
String nameToFind = type.getFullyQualifiedName();
for (ClassNode classNode : classes) {
if (nameToFind.equals(classNode.getName())) {
List<AnnotationNode> annots = classNode
.getAnnotations();
if (annots != null) {
for (AnnotationNode annot : annots) {
ClassNode annotClass = annot
.getClassNode();
if (annotClass != null) {
String annotName = annotClass
.getName();
if (annotName
.equals(TEST_FOR_ANNOT_NAME)) {
return true;
}
}
}
}
}
}
//If we get here the above test failed (i.e annotation wasn't found). Check Grails 2.0 naming conventions
//as well.
if (nameToFind.endsWith("Tests")) {
String domainName = nameToFind.substring(0, nameToFind.length()-"Tests".length());
IType domainType = gcu.getJavaProject().findType(domainName);
if (domainType!=null) {
pfr = (IPackageFragmentRoot) domainType.getAncestor(IJavaElement.PACKAGE_FRAGMENT_ROOT);
return isDomainPackageFragement(pfr);
}
}
}
}
// }
}
} catch (JavaModelException e) {
GrailsCoreActivator.log(e);
}
return false;
}
private boolean isDomainPackageFragement(IPackageFragmentRoot pfr) {
return isPackageFragementWithPath(pfr, "grails-app/domain");
}
private boolean isUnitTestPackageFragement(IPackageFragmentRoot pfr) {
String pfrPath = "test/unit";
return isPackageFragementWithPath(pfr, pfrPath);
}
public boolean isPackageFragementWithPath(IPackageFragmentRoot pfr,
String pfrPath) {
try {
IResource rsrc = pfr.getCorrespondingResource();
if (rsrc!=null) {
IPath path = rsrc.getFullPath();
debug("isGrailsTest pfr path = "+path);
return path.removeFirstSegments(1).equals(new Path(pfrPath));
}
} catch (JavaModelException e) {
GrailsCoreActivator.log(e);
}
return false;
}
/**
* This method determines whether grails aware functionality applies to a given element. If it
* returns false, then methods should just delegate to the original JUnit test finder.
*/
private boolean enableFor(IJavaElement el) {
// debug("enableFor? "+el.getElementName());
IJavaProject javaProject = el.getJavaProject();
if (javaProject!=null) {
// debug("enableFor? "+javaProject.getElementName());
IProject project = javaProject.getProject();
if (GrailsNature.isGrailsProject(project)) {
GrailsVersion version = GrailsVersion.getEclipseGrailsVersion(project);
// debug("enableFor? version = "+version);
boolean result = GrailsVersion.V_2_0_0.compareTo(version) <=0;
// debug("enableFor? => "+result);
return result;
} else {
// debug("enableFor? not a Grails project");
}
}
// debug("enableFor? => false");
return false;
}
}