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