/******************************************************************************* * 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; } }