/******************************************************************************* * 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.rspec; import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.IOException; 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.Matcher; 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.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.dltk.ast.ASTNode; import org.eclipse.dltk.ast.declarations.ModuleDeclaration; import org.eclipse.dltk.ast.expressions.CallArgumentsList; import org.eclipse.dltk.ast.expressions.CallExpression; import org.eclipse.dltk.ast.expressions.NumericLiteral; import org.eclipse.dltk.ast.expressions.StringLiteral; import org.eclipse.dltk.core.DLTKCore; import org.eclipse.dltk.core.IModelElement; import org.eclipse.dltk.core.IScriptProject; import org.eclipse.dltk.core.ISourceModule; import org.eclipse.dltk.core.ISourceRange; import org.eclipse.dltk.core.ModelException; import org.eclipse.dltk.core.SourceRange; 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.RubyASTUtil; import org.eclipse.dltk.ruby.ast.RubyCallArgument; import org.eclipse.dltk.ruby.internal.debug.ui.console.RubyFileHyperlink; import org.eclipse.dltk.ruby.testing.internal.AbstractRubyTestRunnerUI; 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.DLTKTestingPlugin; import org.eclipse.dltk.testing.TestElementResolution; import org.eclipse.dltk.testing.model.ITestCaseElement; import org.eclipse.dltk.testing.model.ITestElement; import org.eclipse.dltk.testing.model.ITestElementPredicate; import org.eclipse.dltk.testing.model.ITestRunSession; import org.eclipse.dltk.testing.model.ITestSuiteElement; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.DefaultLineTracker; import org.eclipse.jface.text.ILineTracker; import org.eclipse.osgi.util.NLS; public class RSpecTestRunnerUI extends AbstractRubyTestRunnerUI { private static final char PATH_BEGIN = '<'; /** * @param testingEngine * @param project */ public RSpecTestRunnerUI(RspecTestingEngine testingEngine, IScriptProject project) { super(testingEngine, project); } @Override public String getTestCaseLabel(ITestCaseElement caseElement, boolean full) { final String testName = caseElement.getTestName(); int index = testName.lastIndexOf(PATH_BEGIN); if (index >= 0) { if (full) { final String template = DLTKTestingMessages.TestSessionLabelProvider_testMethodName_className; return NLS.bind(template, testName.substring(index + 1), testName.substring(0, index)); } else { return testName.substring(0, index); } } return testName; } @Override public String getTestStartedMessage(ITestCaseElement caseElement) { final String testName = caseElement.getTestName(); int index = testName.lastIndexOf(PATH_BEGIN); if (index >= 0) { final String template = DLTKTestingMessages.TestRunnerViewPart_message_started; return NLS.bind(template, testName.substring(index + 1), testName .substring(0, index)); } return testName; } private static class RSpecLocator extends AbstractTestingEngineValidateVisitor { protected ASTNode collectArgs(final CallArgumentsList args, final List<String> texts) { ASTNode lastArg = null; for (Iterator<ASTNode> i = args.getChilds().iterator(); i.hasNext();) { final ASTNode arg = i.next(); if (arg instanceof RubyCallArgument) { final ASTNode value = ((RubyCallArgument) arg).getValue(); final String text = toText(value); if (text != null) { texts.add(text); lastArg = value; } } } return lastArg; } /** * @param value * @return */ private String toText(ASTNode value) { if (value instanceof StringLiteral) { return ((StringLiteral) value).getValue().trim(); } if (value instanceof NumericLiteral) { return ((NumericLiteral) value).getValue(); } return RubyASTUtil.resolveReference(value); } /** * @param value * @return */ protected boolean isMatched(String value, List<String> texts) { final StringBuffer sb = new StringBuffer(); for (Iterator<String> i = texts.iterator(); i.hasNext();) { if (sb.length() != 0) { sb.append(' '); } sb.append(i.next()); } return value.equals(sb.toString()); } public void process(final ISourceModule module) { final ModuleDeclaration declaration = ResolverUtils.parse(module); if (declaration != null) { try { declaration.traverse(this); } catch (Exception e) { RubyTestingPlugin.error("Error in resolveTestSuite", e); //$NON-NLS-1$ } } } } static class RSpecContextLocator extends RSpecLocator { private final String contextName; private ISourceRange range = null; public RSpecContextLocator(String contextName) { this.contextName = contextName; } @Override public boolean visitGeneral(ASTNode node) throws Exception { if (range == null) { if (node instanceof CallExpression) { final CallExpression call = (CallExpression) node; if (isMethodCall(call, RSpecUtils.CONTEXT_METHODS)) { final CallArgumentsList args = call.getArgs(); if (args.getChilds().size() >= 1) { final List<String> texts = new ArrayList<String>(); final ASTNode lastArg = collectArgs(args, texts); if (!texts.isEmpty() && isMatched(contextName, texts)) { assert (lastArg != null); range = new SourceRange(call.sourceStart(), lastArg.sourceEnd() - call.sourceStart()); } } } } } return super.visitGeneral(node); } } private static class RSpecTestLocator extends RSpecLocator { private final String contextName; private final String testName; private ISourceRange range = null; public RSpecTestLocator(String contextName, String testName) { this.contextName = contextName; this.testName = testName; } private static class State { final ASTNode callNode; final boolean isMatched; public State(ASTNode callNode, boolean isMatched) { this.callNode = callNode; this.isMatched = isMatched; } } private final Stack<State> states = new Stack<State>(); @Override public boolean visitGeneral(ASTNode node) throws Exception { if (range == null) { if (node instanceof CallExpression) { final CallExpression call = (CallExpression) node; final CallArgumentsList args = call.getArgs(); if (args.getChilds().size() >= 1) { if (isMethodCall(call, RSpecUtils.CONTEXT_METHODS)) { boolean matched = false; final List<String> texts = new ArrayList<String>(); final ASTNode lastArg = collectArgs(args, texts); if (!texts.isEmpty() && isMatched(contextName, texts)) { assert (lastArg != null); matched = true; // range = new SourceRange(call.sourceStart(), // lastArg.sourceEnd() // - call.sourceStart()); } states.push(new State(node, matched)); } else if (isMatchingContext() && isMethodCall(call, RSpecUtils.TEST_METHODS)) { final List<String> texts = new ArrayList<String>(); final ASTNode lastArg = collectArgs(args, texts); if (!texts.isEmpty() && isMatched(testName, texts)) { assert (lastArg != null); range = new SourceRange(call.sourceStart(), lastArg.sourceEnd() - call.sourceStart()); } } } } } return super.visitGeneral(node); } private boolean isMatchingContext() { if (!states.isEmpty()) { final State state = states.peek(); return state.isMatched; } return false; } @Override public void endvisitGeneral(ASTNode node) throws Exception { if (!states.isEmpty()) { final State state = states.peek(); if (state.callNode == node) { states.pop(); } } super.endvisitGeneral(node); } } private static class MethodRequestor extends SearchRequestor { final Set<IResource> resources = new HashSet<IResource>(); @Override public void acceptSearchMatch(SearchMatch match) throws CoreException { if (match.getResource() != null) { resources.add(match.getResource()); } } } @Override protected TestElementResolution resolveTestSuite(ITestSuiteElement element) { final ITestElement[] children = element.getChildren(); final Set<String> locations = new HashSet<String>(); for (int i = 0; i < children.length; ++i) { if (children[i] instanceof ITestCaseElement) { final ITestCaseElement caseElement = (ITestCaseElement) children[i]; final String testName = caseElement.getTestName(); final int index = testName.lastIndexOf(PATH_BEGIN); if (index > 0) { final String location = testName.substring(index + 1); final Matcher matcher = STACK_FRAME_PATTERN .matcher(location); if (matcher.matches()) { locations.add(matcher.group(1)); } } } } final Set<IResource> processedResources = new HashSet<IResource>(); final RSpecContextLocator locator = new RSpecContextLocator(element .getSuiteTypeName()); for (Iterator<String> i = locations.iterator(); i.hasNext();) { final ISourceModule module = findSourceModule(i.next()); if (module != null) { if (module.getResource() != null) { processedResources.add(module.getResource()); } locator.process(module); if (locator.range != null) { return new TestElementResolution(module, locator.range); } } } final IDLTKSearchScope scope = getSearchScope(); TestElementResolution resolution; resolution = searchMethodReferences(scope, locator, RSpecUtils.DESCRIBE, processedResources); if (resolution != null) { return resolution; } resolution = searchMethodReferences(scope, locator, RSpecUtils.CONTEXT, processedResources); if (resolution != null) { return resolution; } return null; } private TestElementResolution searchMethodReferences( final IDLTKSearchScope scope, final RSpecContextLocator locator, final String methodName, final Set<IResource> processedResources) { final Set<IResource> describeReferences = findMethodReferences(scope, methodName); describeReferences.removeAll(processedResources); for (Iterator<IResource> i = describeReferences.iterator(); i.hasNext();) { final IResource resource = i.next(); if (resource instanceof IFile) { final IFile file = (IFile) resource; processedResources.add(file); final ISourceModule module = (ISourceModule) DLTKCore.create(file); if (module != null) { locator.process(module); if (locator.range != null) { return new TestElementResolution(module, locator.range); } } } } return null; } private Set<IResource> findMethodReferences(final IDLTKSearchScope scope, final String methodName) { final SearchPattern pattern = SearchPattern.createPattern(methodName, IDLTKSearchConstants.METHOD, IDLTKSearchConstants.REFERENCES, SearchPattern.R_EXACT_MATCH | SearchPattern.R_CASE_SENSITIVE, scope.getLanguageToolkit()); final MethodRequestor requestor = new MethodRequestor(); try { new SearchEngine().search(pattern, new SearchParticipant[] { SearchEngine .getDefaultSearchParticipant() }, scope, requestor, null); } catch (CoreException e) { final String msg = "Error in search method references {0})"; //$NON-NLS-1$ RubyTestingPlugin.error(NLS.bind(msg, methodName), e); } final Set<IResource> resources = requestor.resources; return resources; } @Override protected TestElementResolution resolveTestCase(ITestCaseElement element) { if (!(element.getParentContainer() instanceof ITestSuiteElement)) { return null; } final String testName = element.getTestName(); final int index = testName.lastIndexOf(PATH_BEGIN); if (index < 0) { return null; } final String location = testName.substring(index + 1); final Matcher matcher = STACK_FRAME_PATTERN.matcher(location); if (!matcher.matches()) { return null; } final ISourceModule module = findSourceModule(matcher.group(1)); if (module == null) { return null; } final RSpecTestLocator locator = new RSpecTestLocator( ((ITestSuiteElement) element.getParentContainer()) .getSuiteTypeName(), testName.substring(0, index)); locator.process(module); if (locator.range != null) { return new TestElementResolution(module, locator.range); } final String source; try { source = module.getSource(); } catch (ModelException e) { return null; } final ILineTracker lineTracker = new DefaultLineTracker(); lineTracker.set(source); final int lineNumber; try { lineNumber = Integer.parseInt(matcher.group(2)); } catch (NumberFormatException e) { return null; } org.eclipse.jface.text.IRegion line; try { line = lineTracker.getLineInformation(lineNumber - 1); } catch (BadLocationException e) { return null; } return new TestElementResolution(module, ResolverUtils.adjustRange( source, line.getOffset(), line.getOffset() + line.getLength())); } private ISourceModule findSourceModule(String path) { final Object result = RubyFileHyperlink.findSourceModule(path); if (result instanceof ISourceModule) { return (ISourceModule) result; } if (result instanceof IFile) { IModelElement element = DLTKCore.create((IFile) result); if (element instanceof ISourceModule) { return (ISourceModule) element; } } return null; } private static String buildRegex() { final String slash = "[\\\\/]"; //$NON-NLS-1$ return slash + "gems" + slash + "rspec-[\\w\\.]+" + slash + "lib" //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + slash; } private static final Pattern GEM_RSPEC_LIB = Pattern.compile(buildRegex()); @Override protected boolean selectLine(String line) { final String filename = extractFileName(line); if (filename == null) { return true; } if (filename.endsWith(RspecTestingEngine.RSPEC_RUNNER)) { return false; } if (GEM_RSPEC_LIB.matcher(filename).find()) { return false; } return true; } @Override public boolean canRerunFailures() { return true; } @Override public String collectFailures(ITestRunSession testRunSession) throws CoreException { try { final File file = File.createTempFile("rspecTestFailures", ".txt"); //$NON-NLS-1$ //$NON-NLS-2$ file.deleteOnExit(); BufferedWriter bw = null; try { bw = new BufferedWriter(new FileWriter(file)); final ITestElement[] failures = testRunSession .getFailedTestElements(new ITestElementPredicate() { @Override public boolean matches(ITestElement testElement) { return testElement instanceof ITestCaseElement; } }); for (int i = 0; i < failures.length; i++) { final ITestElement failure = failures[i]; if (failure instanceof ITestCaseElement && failure.getParentContainer() instanceof ITestSuiteElement) { final ITestSuiteElement suite = (ITestSuiteElement) failure .getParentContainer(); final String exampleName = suite.getSuiteTypeName() + " " //$NON-NLS-1$ + getTestCaseLabel((ITestCaseElement) failure, false); bw.write(exampleName); bw.newLine(); // TODO handle "automatic" example names } } } finally { if (bw != null) { bw.close(); } } return file.getAbsolutePath(); } catch (IOException e) { throw new CoreException(new Status(IStatus.ERROR, DLTKTestingPlugin.PLUGIN_ID, IStatus.ERROR, "", e)); //$NON-NLS-1$ } } }