blob: d3bc6af2c052f437fc8ffe1428dacd9e6e39a29e [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2008, 2016 xored software, Inc. and others
*
* 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:
* xored software, Inc. - initial API and Implementation (Alex Panchenko)
*******************************************************************************/
package org.eclipse.dltk.ruby.testing.internal.testunit;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.Stack;
import java.util.regex.Pattern;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.dltk.ast.ASTNode;
import org.eclipse.dltk.ast.declarations.ModuleDeclaration;
import org.eclipse.dltk.ast.declarations.TypeDeclaration;
import org.eclipse.dltk.ast.expressions.CallExpression;
import org.eclipse.dltk.ast.expressions.StringLiteral;
import org.eclipse.dltk.compiler.util.Util;
import org.eclipse.dltk.core.DLTKCore;
import org.eclipse.dltk.core.IMethod;
import org.eclipse.dltk.core.IProjectFragment;
import org.eclipse.dltk.core.IScriptProject;
import org.eclipse.dltk.core.ISourceModule;
import org.eclipse.dltk.core.ISourceRange;
import org.eclipse.dltk.core.IType;
import org.eclipse.dltk.core.ModelException;
import org.eclipse.dltk.core.SourceRange;
import org.eclipse.dltk.core.environment.EnvironmentPathUtils;
import org.eclipse.dltk.core.search.IDLTKSearchConstants;
import org.eclipse.dltk.core.search.IDLTKSearchScope;
import org.eclipse.dltk.core.search.SearchEngine;
import org.eclipse.dltk.core.search.SearchMatch;
import org.eclipse.dltk.core.search.SearchParticipant;
import org.eclipse.dltk.core.search.SearchPattern;
import org.eclipse.dltk.core.search.SearchRequestor;
import org.eclipse.dltk.ruby.ast.RubyCallArgument;
import org.eclipse.dltk.ruby.core.utils.RubySyntaxUtils;
import org.eclipse.dltk.ruby.internal.debug.ui.console.RubyConsoleSourceModuleLookup;
import org.eclipse.dltk.ruby.testing.internal.AbstractRubyTestRunnerUI;
import org.eclipse.dltk.ruby.testing.internal.AbstractRubyTestingEngine;
import org.eclipse.dltk.ruby.testing.internal.AbstractTestingEngineValidateVisitor;
import org.eclipse.dltk.ruby.testing.internal.ResolverUtils;
import org.eclipse.dltk.ruby.testing.internal.RubyTestingPlugin;
import org.eclipse.dltk.testing.DLTKTestingMessages;
import org.eclipse.dltk.testing.TestElementResolution;
import org.eclipse.dltk.testing.model.ITestCaseElement;
import org.eclipse.dltk.testing.model.ITestSuiteElement;
import org.eclipse.osgi.util.NLS;
public class TestUnitTestRunnerUI extends AbstractRubyTestRunnerUI {
private static final char CLASS_BEGIN = '(';
private static final char CLASS_END = ')';
/**
* @param testingEngine
*/
public TestUnitTestRunnerUI(AbstractRubyTestingEngine testingEngine,
IScriptProject project) {
super(testingEngine, project);
}
@Override
public String getTestCaseLabel(ITestCaseElement caseElement, boolean full) {
final String testName = caseElement.getTestName();
int index = testName.lastIndexOf(CLASS_BEGIN);
if (index > 0) {
final int braceIndex = index;
while (index > 0
&& Character.isWhitespace(testName.charAt(index - 1))) {
--index;
}
if (full) {
int end = testName.length();
if (end > braceIndex + 1
&& testName.charAt(end - 1) == CLASS_END) {
--end;
}
final String template = DLTKTestingMessages.TestSessionLabelProvider_testMethodName_className;
return NLS.bind(template, testName.substring(braceIndex + 1,
end), testName.substring(0, index));
} else {
return testName.substring(0, index);
}
} else {
return testName;
}
}
@Override
public String getTestStartedMessage(ITestCaseElement caseElement) {
final String testName = caseElement.getTestName();
int index = testName.lastIndexOf(CLASS_BEGIN);
if (index > 0) {
int end = testName.length();
if (end > index && testName.charAt(end - 1) == CLASS_END) {
--end;
}
final String className = testName.substring(index + 1, end);
while (index > 0
&& Character.isWhitespace(testName.charAt(index - 1))) {
--index;
}
final String method = testName.substring(0, index);
return NLS.bind(
DLTKTestingMessages.TestRunnerViewPart_message_started,
className, method);
} else {
return testName;
}
}
private static final String SHOULDA_TEST_PREFIX = "test:"; //$NON-NLS-1$
@Override
protected TestElementResolution resolveTestCase(ITestCaseElement testCase) {
final String testName = testCase.getTestName();
if (testName.length() == 0) {
return null;
}
final int pos = testName.lastIndexOf(CLASS_BEGIN);
if (!(pos > 0 && testName.charAt(testName.length() - 1) == CLASS_END)) {
return null;
}
final String className = testName.substring(pos + 1,
testName.length() - 1);
if (!RubySyntaxUtils.isValidClass(className)) {
return null;
}
final String methodName = testName.substring(0, pos).trim();
if (RubySyntaxUtils.isRubyMethodName(methodName)) {
final IMethod method = findMethod(className, methodName);
if (method != null) {
return new TestElementResolution(method, ResolverUtils
.getSourceRange(method));
}
}
final List types = findClasses(className);
if (types == null) {
return null;
}
if (methodName.startsWith(SHOULDA_TEST_PREFIX)) {
String shouldName = methodName.substring(
SHOULDA_TEST_PREFIX.length()).trim();
if (shouldName.length() != 0
&& shouldName.charAt(shouldName.length() - 1) == '.') {
shouldName = shouldName.substring(0, shouldName.length() - 1)
.trim();
}
if (shouldName.length() != 0) {
final Set<IFile> resources = new HashSet<IFile>();
for (Iterator i = types.iterator(); i.hasNext();) {
final IType type = (IType) i.next();
final IResource resource = type.getResource();
if (resource != null && resource instanceof IFile) {
resources.add((IFile) resource);
}
}
if (resources.isEmpty()) {
return null;
}
for (Iterator<IFile> i = resources.iterator(); i.hasNext();) {
final ISourceModule module = (ISourceModule) DLTKCore.create(i.next());
final TestElementResolution resolution = findShould(module,
className, shouldName);
if (resolution != null) {
return resolution;
}
}
}
}
return null;
}
private static class ShouldLocator extends
AbstractTestingEngineValidateVisitor {
private static final String TWO_COLONS = "::"; //$NON-NLS-1$
private final String className;
private final String shouldName;
private ISourceRange range = null;
/**
* @param className
* @param shouldName
*/
public ShouldLocator(String className, String shouldName) {
this.className = className;
this.shouldName = shouldName;
}
final Stack<Boolean> typeMatches = new Stack<Boolean>();
@Override
public boolean visit(TypeDeclaration s) throws Exception {
final String enclosingName = s.getEnclosingTypeName();
final String fullName;
if (enclosingName.length() == 0) {
fullName = s.getName();
} else {
fullName = enclosingName.replaceAll("\\$", TWO_COLONS) + TWO_COLONS + s.getName(); //$NON-NLS-1$
}
typeMatches.push(Boolean.valueOf(className.equals(fullName)));
return true;
}
@Override
public boolean endvisit(TypeDeclaration s) throws Exception {
typeMatches.pop();
return true;
}
private boolean isMatchedType() {
for (int i = 0, size = typeMatches.size(); i < size; ++i) {
Boolean value = typeMatches.get(i);
if (value.booleanValue()) {
return true;
}
}
return false;
}
final Stack<CallExpression> calls = new Stack<CallExpression>();
@Override
public boolean visitGeneral(ASTNode node) throws Exception {
if (isMatchedType() && range == null) {
if (node instanceof CallExpression) {
final CallExpression call = (CallExpression) node;
if (isMethodCall(call, ShouldaUtils.METHODS)
&& call.getArgs().getChilds().size() >= 1) {
final Object arg0 = call.getArgs().getChilds().get(0);
if (arg0 instanceof RubyCallArgument) {
final RubyCallArgument callArg = (RubyCallArgument) arg0;
if (callArg.getValue() instanceof StringLiteral) {
calls.push(call);
if (isShouldMatched()) {
range = new SourceRange(call.sourceStart(),
callArg.sourceEnd()
- call.sourceStart());
}
}
}
}
}
}
return super.visitGeneral(node);
}
/**
* @return
*/
private boolean isShouldMatched() {
if (isShouldMatched(shouldName)) {
return true;
}
final String noTestClassName = className.replaceAll(
"Test", Util.EMPTY_STRING); //$NON-NLS-1$
if (startsWith(shouldName, noTestClassName)) {
return isShouldMatched(shouldName.substring(
noTestClassName.length()).trim());
}
return false;
}
private boolean startsWith(final String value, final String substring) {
return value.length() > substring.length()
&& value.startsWith(substring)
&& Character.isWhitespace(value.charAt(substring.length()));
}
/**
* @param value
* @return
*/
private boolean isShouldMatched(String value) {
for (int i = 0; i < calls.size(); ++i) {
final CallExpression call = calls.get(i);
if (ShouldaUtils.SHOULD.equals(call.getName())) {
if (!startsWith(value, ShouldaUtils.SHOULD)) {
return false;
}
value = value.substring(ShouldaUtils.SHOULD.length())
.trim();
final RubyCallArgument callArg = (RubyCallArgument) call
.getArgs().getChilds().get(0);
final String literal = ((StringLiteral) callArg.getValue())
.getValue();
if (value.equals(literal)) {
return true;
}
} else if (ShouldaUtils.CONTEXT.equals(call.getName())) {
final RubyCallArgument callArg = (RubyCallArgument) call
.getArgs().getChilds().get(0);
final String literal = ((StringLiteral) callArg.getValue())
.getValue().trim();
if (!startsWith(value, literal)) {
return false;
}
value = value.substring(literal.length()).trim();
}
}
return false;
}
@Override
public void endvisitGeneral(ASTNode node) throws Exception {
if (!calls.isEmpty() && calls.peek() == node) {
calls.pop();
}
super.endvisitGeneral(node);
}
}
/**
* @param module
* @param className
* @param shouldName
* @return
*/
private TestElementResolution findShould(ISourceModule module,
String className, String shouldName) {
final ModuleDeclaration declaration = ResolverUtils.parse(module);
if (declaration != null) {
try {
final ShouldLocator locator = new ShouldLocator(className,
shouldName);
declaration.traverse(locator);
if (locator.range != null) {
final ISourceRange range = ResolverUtils.adjustRange(module
.getSource(), locator.range);
return new TestElementResolution(module, range);
}
} catch (Exception e) {
final String msg = "Error in findShould()"; //$NON-NLS-1$
RubyTestingPlugin.error(msg, e);
}
}
return null;
}
@Override
protected TestElementResolution resolveTestSuite(ITestSuiteElement element) {
final String className = element.getSuiteTypeName();
if (RubySyntaxUtils.isValidClass(className)) {
final List types = findClasses(className);
if (types != null) {
final IType type = (IType) types.get(0);
return new TestElementResolution(type, ResolverUtils
.getSourceRange(type));
}
}
return null;
}
private static final class TypeSearchRequestor extends SearchRequestor {
final List<Object> types = new ArrayList<Object>();
@Override
public void acceptSearchMatch(SearchMatch match) throws CoreException {
types.add(match.getElement());
}
}
private static final class MethodRequestor extends SearchRequestor {
IMethod method = null;
@Override
public void acceptSearchMatch(SearchMatch match) throws CoreException {
method = (IMethod) match.getElement();
}
}
/**
* @param className
* @param methodName
* @return
*/
private IMethod findMethod(String className, String methodName) {
final IDLTKSearchScope scope = getSearchScope();
final String sPattern = className + "::" + methodName; //$NON-NLS-1$
SearchPattern pattern = SearchPattern.createPattern(sPattern,
IDLTKSearchConstants.METHOD, IDLTKSearchConstants.DECLARATIONS,
SearchPattern.R_EXACT_MATCH | SearchPattern.R_CASE_SENSITIVE,
scope.getLanguageToolkit());
try {
final MethodRequestor requestor = new MethodRequestor();
new SearchEngine().search(pattern,
new SearchParticipant[] { SearchEngine
.getDefaultSearchParticipant() }, scope, requestor,
null);
return requestor.method;
} catch (CoreException e) {
final String msg = "Error in findMethod({0}::{1})"; //$NON-NLS-1$
RubyTestingPlugin.error(NLS.bind(msg, className, methodName), e);
}
return null;
}
/**
* @param className
*/
private List findClasses(String className) {
final IDLTKSearchScope scope = getSearchScope();
SearchPattern pattern = SearchPattern.createPattern(className,
IDLTKSearchConstants.TYPE, IDLTKSearchConstants.DECLARATIONS,
SearchPattern.R_EXACT_MATCH | SearchPattern.R_CASE_SENSITIVE,
scope.getLanguageToolkit());
try {
final TypeSearchRequestor requestor = new TypeSearchRequestor();
new SearchEngine().search(pattern,
new SearchParticipant[] { SearchEngine
.getDefaultSearchParticipant() }, scope, requestor,
null);
if (!requestor.types.isEmpty()) {
return requestor.types;
}
} catch (CoreException e) {
final String msg = "Error in findClasses({0})"; //$NON-NLS-1$
RubyTestingPlugin.error(NLS.bind(msg, className), e);
}
return null;
}
private static final String[] TEST_UNIT = { "test", "unit" }; //$NON-NLS-1$ //$NON-NLS-2$
private boolean testFragmentPath(IPath fragmentPath, IPath path) {
if (pathEquality.isPrefixOf(fragmentPath, path)
&& path.segmentCount() > fragmentPath.segmentCount()
+ TEST_UNIT.length) {
for (int j = 0; j < TEST_UNIT.length; ++j) {
if (!TEST_UNIT[j].equals(path.segment(fragmentPath
.segmentCount()
+ j))) {
return false;
}
}
return true;
}
return false;
}
private static String buildRegex() {
final String slash = "[\\\\/]"; //$NON-NLS-1$
return slash + "gems" + slash + "Shoulda-[\\w\\.]+" + slash + "lib" //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+ slash;
}
private static final Pattern GEM_SHOULDA_LIB = Pattern
.compile(buildRegex());
@Override
protected boolean selectLine(String line) {
final String filename = extractFileName(line);
if (filename == null) {
return true;
}
if (filename.endsWith(TestUnitTestingEngine.TEST_UNIT_RUNNER)) {
return false;
}
if (GEM_SHOULDA_LIB.matcher(filename).find()) {
return false;
}
final IPath path = new Path(filename);
try {
final IProjectFragment[] fragments = project.getProjectFragments();
for (int i = 0; i < fragments.length; ++i) {
final IProjectFragment fragment = fragments[i];
if (fragment.isExternal()
&& testFragmentPath(EnvironmentPathUtils
.getLocalPath(fragment.getPath()), path)
&& RubyConsoleSourceModuleLookup.isIncluded(fragment,
path)) {
return false;
}
}
} catch (ModelException e) {
return true;
}
return true;
}
}