From 6432459427a5bad3f2da062b53b393f51c5c616d Mon Sep 17 00:00:00 2001 From: Xavier Coulon Date: Mon, 9 Sep 2013 15:31:37 +0200 Subject: [PATCH] Bug 372588 - [JUnit] Add 'Link with Editor' to JUnit view Adding a toggle action to enable/disable 'Link with Editor' (disabled by default) When 'Linking with editor' is enabled, an inner IPartListener2 is added to the active page When the editor selection changes (user switching to another Java editor or moving selection in another method of the same Test class), the selection in the TestViewer is updated accordingly (if necessary). Likewise, when the selection in the TestViewer changes, the Java Editor containing the test method is selected (if it has been opened before) and the test method is selected. Signed-off-by: Xavier Coulon --- .../jdt/internal/junit/ui/TestRunnerViewPart.java | 317 ++++++++++++++++++++- .../eclipse/jdt/internal/junit/ui/TestViewer.java | 48 ++++ 2 files changed, 363 insertions(+), 2 deletions(-) diff --git a/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/ui/TestRunnerViewPart.java b/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/ui/TestRunnerViewPart.java index ccf104e..2af35cf 100644 --- a/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/ui/TestRunnerViewPart.java +++ b/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/ui/TestRunnerViewPart.java @@ -71,6 +71,7 @@ import org.eclipse.core.commands.IHandler; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Platform; @@ -79,6 +80,8 @@ import org.eclipse.core.runtime.jobs.ILock; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.core.runtime.preferences.InstanceScope; +import org.eclipse.core.resources.IFile; + import org.eclipse.jface.action.Action; import org.eclipse.jface.action.IAction; import org.eclipse.jface.action.IMenuListener; @@ -96,10 +99,18 @@ import org.eclipse.jface.dialogs.InputDialog; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.operation.IRunnableWithProgress; import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.viewers.IPostSelectionProvider; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.jface.viewers.ISelectionProvider; +import org.eclipse.jface.viewers.SelectionChangedEvent; + +import org.eclipse.jface.text.ITextSelection; import org.eclipse.ui.IActionBars; import org.eclipse.ui.IEditorActionBarContributor; import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IEditorReference; import org.eclipse.ui.IMemento; import org.eclipse.ui.IPartListener2; import org.eclipse.ui.IViewPart; @@ -115,6 +126,7 @@ import org.eclipse.ui.actions.ActionFactory; import org.eclipse.ui.handlers.IHandlerActivation; import org.eclipse.ui.handlers.IHandlerService; import org.eclipse.ui.part.EditorActionBarContributor; +import org.eclipse.ui.part.FileEditorInput; import org.eclipse.ui.part.PageSwitcher; import org.eclipse.ui.part.ViewPart; import org.eclipse.ui.progress.IWorkbenchSiteProgressService; @@ -128,11 +140,16 @@ import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; import org.eclipse.debug.ui.DebugUITools; import org.eclipse.jdt.core.ElementChangedEvent; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.ICompilationUnit; import org.eclipse.jdt.core.IElementChangedListener; import org.eclipse.jdt.core.IJavaElement; import org.eclipse.jdt.core.IJavaElementDelta; import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IMethod; +import org.eclipse.jdt.core.IType; import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.internal.junit.BasicElementLabels; import org.eclipse.jdt.internal.junit.JUnitCorePlugin; @@ -149,6 +166,10 @@ import org.eclipse.jdt.internal.junit.model.TestRunSession; import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants; +import org.eclipse.jdt.ui.JavaUI; + +import org.eclipse.jdt.internal.ui.actions.AbstractToggleLinkingAction; +import org.eclipse.jdt.internal.ui.javaeditor.JavaEditor; import org.eclipse.jdt.internal.ui.viewsupport.ViewHistory; /** @@ -210,6 +231,9 @@ public class TestRunnerViewPart extends ViewPart { private Action fPreviousAction; private StopAction fStopAction; + + private LinkWithEditorAction fLinkWithEditorAction; + private JUnitCopyAction fCopyAction; private Action fPasteAction; @@ -342,7 +366,6 @@ public class TestRunnerViewPart extends ViewPart { protected boolean fPartIsVisible= false; - private class RunnerViewHistory extends ViewHistory { @Override @@ -705,6 +728,8 @@ public class TestRunnerViewPart extends ViewPart { startUpdateJobs(); fStopAction.setEnabled(true); + fLinkWithEditorAction.setEnabled(true); + fRerunLastTestAction.setEnabled(true); } @@ -722,6 +747,7 @@ public class TestRunnerViewPart extends ViewPart { if (isDisposed()) return; fStopAction.setEnabled(lastLaunchIsKeptAlive()); + fLinkWithEditorAction.setEnabled(fTestRunSession != null); updateRerunFailedFirstAction(); processChangesInUI(); if (hasErrorsOrFailures()) { @@ -899,6 +925,14 @@ public class TestRunnerViewPart extends ViewPart { } } + private class LinkWithEditorAction extends AbstractToggleLinkingAction { + + @Override + public void run() { + setLinkingEnabled(isChecked()); + } + } + private class RerunLastAction extends Action { public RerunLastAction() { setText(JUnitMessages.TestRunnerViewPart_rerunaction_label); @@ -1200,6 +1234,178 @@ public class TestRunnerViewPart extends ViewPart { } } + /** + * Links the selected test method with the Java Editor + * @param enabled boolean to indicate if the link with editor is enabled (true) or not (false) + */ + public void setLinkingEnabled(boolean enabled) { + final IWorkbenchPage page = getSite().getPage(); + if(page == null) { + return; + } + if (enabled) { + // add an IPartListener for future editor activations/opening/closing/etc. + page.addPartListener(fLinkWithEditorPartListener); + } else { + // removes the IPartListener + page.removePartListener(fLinkWithEditorPartListener); + } + for(IEditorReference editorReference : page.getEditorReferences()) { + final IEditorPart editor = editorReference.getEditor(false); + // set the current editor as active as well + if (editor != null && enabled) { + editorActivated(editor); + } + // unset the current editor as active as well + else if(editor != null && !enabled){ + editorDeactivated(editor); + } + } + } + + + private final IPartListener2 fLinkWithEditorPartListener= new IPartListener2() { + public void partVisible(IWorkbenchPartReference partRef) {} + public void partBroughtToTop(IWorkbenchPartReference partRef) {} + public void partHidden(IWorkbenchPartReference partRef) {} + public void partOpened(IWorkbenchPartReference partRef) {} + public void partInputChanged(IWorkbenchPartReference partRef) {} + public void partClosed(IWorkbenchPartReference partRef) {} + + public void partActivated(IWorkbenchPartReference partRef) { + if (partRef instanceof IEditorReference) { + editorActivated(((IEditorReference) partRef).getEditor(true)); + } + } + + public void partDeactivated(IWorkbenchPartReference partRef) { + if (partRef instanceof IEditorReference) { + editorDeactivated(((IEditorReference) partRef).getEditor(true)); + } + } + + }; + + /** + * Static Inner Java Editor selection listener + * When the user moves to another + */ + private class JavaEditorSelectionListener implements ISelectionChangedListener { + + private final ICompilationUnit compilationUnit; + + JavaEditorSelectionListener(final ICompilationUnit compilationUnit) { + this.compilationUnit = compilationUnit; + } + + public void selectionChanged(final SelectionChangedEvent event) { + selectActiveTestCaseElement(event.getSelection()); + } + + private IJavaElement getActiveJavaElement(final ISelection selection) { + if(selection instanceof ITextSelection) { + final int offset= ((ITextSelection)selection).getOffset(); + try { + return compilationUnit.getElementAt(offset); + } catch (JavaModelException e) { + JUnitPlugin.log(e); + } + } + return null; + } + + public void selectActiveTestCaseElement(final ISelection selection) { + final IJavaElement activeJavaElement = getActiveJavaElement(selection); + // select the method in the JUnit ViewPart + if(activeJavaElement != null && activeJavaElement.getElementType() == IJavaElement.METHOD) { + final IMethod activeJavaMethod = (IMethod)activeJavaElement; + final IType activeJavaType = (IType)activeJavaMethod.getAncestor(IJavaElement.TYPE); + fTestViewer.selectTestCaseElement(activeJavaType.getFullyQualifiedName(), activeJavaMethod.getElementName()); + } + + } + + /** + * Overriding java.lang.Object#hashCode() using the + * {@link ICompilationUnit#getHandleIdentifier()} value to avoid duplicate listener + * registrations in the {@link JavaEditor}'s {@link IPostSelectionProvider}. + */ + @Override + public int hashCode() { + final int prime= 31; + int result= 1; + result= prime * result + ((compilationUnit == null) ? 0 : compilationUnit.getHandleIdentifier().hashCode()); + return result; + } + + /** + * Overriding java.lang.Object#equals() using the + * {@link ICompilationUnit#getHandleIdentifier()} value to avoid duplicate listener + * registrations in the {@link JavaEditor}'s {@link IPostSelectionProvider}. + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + JavaEditorSelectionListener other= (JavaEditorSelectionListener)obj; + if (compilationUnit == null) { + if (other.compilationUnit != null) { + return false; + } + } else if (!compilationUnit.getHandleIdentifier().equals(other.compilationUnit.getHandleIdentifier())) { + return false; + } + return true; + } + + + } + + private JavaEditorSelectionListener fJavaEditorSelectionListener; + + /** + * An editor has been activated. Set the selection in this JUnit ViewPart + * to match the editor's input, if linking is enabled. + * @param editor the activated editor + */ + private void editorActivated(IEditorPart editor) { + if(!(editor instanceof JavaEditor)) { + return; + } + final ISelectionProvider selectionProvider = ((JavaEditor)editor).getSelectionProvider(); + final Object input= JavaUI.getEditorInputJavaElement(editor.getEditorInput()); + if (input instanceof ICompilationUnit && selectionProvider instanceof IPostSelectionProvider) { + final ICompilationUnit unit = (ICompilationUnit) input; + final IPostSelectionProvider postSelectionProvider = (IPostSelectionProvider)selectionProvider; + fJavaEditorSelectionListener = new JavaEditorSelectionListener(unit); + postSelectionProvider.addPostSelectionChangedListener(fJavaEditorSelectionListener); + fJavaEditorSelectionListener.selectActiveTestCaseElement(selectionProvider.getSelection()); + } + } + + /** + * An editor has been deactivated. + * @param editor the activated editor + */ + private void editorDeactivated(IEditorPart editor) { + if(!(editor instanceof JavaEditor)) { + return; + } + final ISelectionProvider selectionProvider = ((JavaEditor)editor).getSelectionProvider(); + if (selectionProvider instanceof IPostSelectionProvider) { + final IPostSelectionProvider postSelectionProvider = (IPostSelectionProvider)selectionProvider; + postSelectionProvider.removePostSelectionChangedListener(fJavaEditorSelectionListener); + } + } + + private void startUpdateJobs() { postSyncProcessChanges(); @@ -1507,11 +1713,13 @@ action enablement startUpdateJobs(); fStopAction.setEnabled(true); + fLinkWithEditorAction.setEnabled(true); } else /* old or fresh session: don't want jobs at this stage */ { stopUpdateJobs(); fStopAction.setEnabled(fTestRunSession.isKeptAlive()); + fLinkWithEditorAction.setEnabled(fTestRunSession != null); fTestViewer.expandFirstLevel(); } } @@ -1887,6 +2095,9 @@ action enablement fStopAction= new StopAction(); fStopAction.setEnabled(false); + fLinkWithEditorAction= new LinkWithEditorAction(); + fLinkWithEditorAction.setEnabled(false); + fRerunLastTestAction= new RerunLastAction(); IHandlerService handlerService= (IHandlerService) getSite().getWorkbenchWindow().getService(IHandlerService.class); IHandler handler = new AbstractHandler() { @@ -1937,7 +2148,9 @@ action enablement toolBar.add(fRerunFailedFirstAction); toolBar.add(fStopAction); toolBar.add(fViewHistory.createHistoryDropDownAction()); - + toolBar.add(new Separator()); + toolBar.add(fLinkWithEditorAction); + viewMenu.add(fShowTestHierarchyAction); viewMenu.add(fShowTimeAction); @@ -2007,6 +2220,106 @@ action enablement public void handleTestSelected(TestElement test) { showFailure(test); fCopyAction.handleTestSelected(test); + // if LinkWithEditor is active, reveal the JavaEditor and select the java method + // matching the selected test element, even if the JavaEditor is opened by not active. + if(fLinkWithEditorAction.isChecked() && test instanceof TestCaseElement) { + try { + final TestCaseElement testCaseElement= (TestCaseElement)test; + for (IEditorReference editorReference : PlatformUI.getWorkbench().getActiveWorkbenchWindow() + .getActivePage().getEditorReferences()) { + final IEditorPart editor = editorReference.getEditor(true); + final IMethod selectedMethod = findSelectedMethodInEditor(editor, testCaseElement); + if(selectedMethod != null) { + JavaUI.openInEditor(selectedMethod, false, true); + break; + } + } + } catch(JavaModelException e) { + JUnitPlugin.log(e); + } catch(PartInitException e) { + JUnitPlugin.log(e); + } + } + } + + + /** + * Finds and returns the selected {@link IMethod} matching the given {@link TestCaseElement} in + * the given {@link IEditorPart} (if this later is a {@link JavaEditor}), or returns null if the + * editor is not a {@link JavaEditor} or is not opened on the expected {@link ICompilationUnit}. + * + * @param editor the editor to inspect + * @param test the selected test case element + * @return the corresponding Java method element or null + * @throws JavaModelException if the inspection of one of the java elements in the given editor failed. + */ + private IMethod findSelectedMethodInEditor(IEditorPart editor, TestCaseElement test) throws JavaModelException { + final String editorClassName = getJavaEditorClassName(editor); + final String testMethodName= getTestMethodName(test); + if(test.getClassName().equals(editorClassName)) { + final IJavaElement editorInputJavaElement= JavaUI.getEditorInputJavaElement(editor.getEditorInput()); + if (editorInputJavaElement.getElementType() == IJavaElement.COMPILATION_UNIT) { + for (IType type : ((ICompilationUnit)editorInputJavaElement).getTypes()) { + if (type.getFullyQualifiedName().equals(test.getClassName())) { + for (IMethod method : type.getMethods()) { + if (method.getElementName().equals(testMethodName)) { + return method; + } + } + } + } + } + } + return null; + } + + /** + * @return the fully qualified name of the Java class opened by the editor if it is a JavaEditor, null otherwise. + * @param editor the editor to inspect + */ + private String getJavaEditorClassName(final IEditorPart editor) { + if(editor instanceof JavaEditor && editor.getEditorInput() instanceof FileEditorInput) { + final JavaEditor javaEditor = (JavaEditor)editor; + return getEditorClassName(javaEditor); + } + return null; + } + + /** + * @return the name of the Java method associated with this test. + * @param test the selected unit test + */ + private String getTestMethodName(TestCaseElement test) { + final String testMethodName = test.getTestMethodName(); + // parameterized tests show an index that should be removed from the name + final int index = testMethodName.indexOf('['); + if(index != -1) { + return testMethodName.substring(0, index); + } + return testMethodName; + } + + /** + * @return the Java classname associated with the file opened in the given {@link JavaEditor}. + * @param javaEditor the javaEditor in which the file is opened. + */ + private String getEditorClassName(final JavaEditor javaEditor) { + try { + final FileEditorInput editorInput = (FileEditorInput)javaEditor.getEditorInput(); + final IFile inputFile = editorInput.getFile(); + IPath inputFilePath = inputFile.getFullPath(); + IJavaProject javaProject = JavaCore.create(inputFile.getProject()); + for(IClasspathEntry classpathEntry: javaProject.getRawClasspath()) { + if(classpathEntry.getEntryKind() == IClasspathEntry.CPE_SOURCE && classpathEntry.getPath().isPrefixOf(inputFilePath)) { + IPath sourcePath = inputFilePath.makeRelativeTo(classpathEntry.getPath()); + String sourceClassName= sourcePath.removeFileExtension().toOSString().replaceAll("/", "."); //$NON-NLS-1$ //$NON-NLS-2$ + return sourceClassName; + } + } + } catch (JavaModelException e) { + JUnitPlugin.log(e); + } + return null; } private void showFailure(final TestElement test) { diff --git a/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/ui/TestViewer.java b/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/ui/TestViewer.java index e050d66..2d6c576 100644 --- a/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/ui/TestViewer.java +++ b/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/ui/TestViewer.java @@ -21,7 +21,9 @@ import java.util.LinkedList; import java.util.List; import java.util.ListIterator; +import org.eclipse.jdt.junit.model.ITestCaseElement; import org.eclipse.jdt.junit.model.ITestElement; +import org.eclipse.jdt.junit.model.ITestSuiteElement; import org.eclipse.swt.SWT; import org.eclipse.swt.dnd.Clipboard; @@ -38,6 +40,7 @@ import org.eclipse.jface.action.IMenuManager; import org.eclipse.jface.action.MenuManager; import org.eclipse.jface.action.Separator; import org.eclipse.jface.viewers.AbstractTreeViewer; +import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.SelectionChangedEvent; @@ -568,6 +571,51 @@ public class TestViewer { fTreeViewer.reveal(current); } + /** + * Selects and reveals the given {@link ITestCaseElement} in the viewer's tree. + * @param testClassName the qualified class name of the test to select + * @param testMethodName the method name of the test to select + */ + public void selectTestCaseElement(final String testClassName, final String testMethodName) { + // skip if history was cleared and 'Link with Editor' is still enabled. + if(fTestRunSession == null) { + return; + } + final TestRoot testRoot = fTestRunSession.getTestRoot(); + final ISelection currentSelection= getActiveViewer().getSelection(); + if(currentSelection instanceof IStructuredSelection) { + for(Object selectedItem : ((IStructuredSelection)currentSelection).toList()) { + if(selectedItem instanceof TestCaseElement && ((TestCaseElement)selectedItem).getTestClassName().equals(testClassName) + && ((TestCaseElement)selectedItem).getTestMethodName().equals(testMethodName)) { + // the current selection in the TestViewer includes the given test class/method + return; + } + } + } + final ITestCaseElement activeTestCaseElement = findTestCaseElement(testRoot, testClassName, testMethodName); + if(activeTestCaseElement != null) { + getActiveViewer().setSelection(new StructuredSelection(activeTestCaseElement), true); + } + } + + private ITestCaseElement findTestCaseElement(final ITestSuiteElement parentElement, final String testClassName, final String testMethodName) { + for(ITestElement childElement : parentElement.getChildren()) { + if(childElement instanceof ITestCaseElement) { + ITestCaseElement testCaseElement = (ITestCaseElement)childElement; + if(testCaseElement.getTestClassName().equals(testClassName) && testCaseElement.getTestMethodName().equals(testMethodName)) { + return testCaseElement; + } + } else if(childElement instanceof ITestSuiteElement) { + final ITestCaseElement localResult= findTestCaseElement((ITestSuiteElement)childElement, testClassName, testMethodName); + if(localResult != null) { + return localResult; + } + } + } + return null; + + } + public void selectFirstFailure() { TestCaseElement firstFailure= getNextChildFailure(fTestRunSession.getTestRoot(), true); if (firstFailure != null) -- 1.7.12.4 (Apple Git-37)